시계 위젯 설정을 팝오버로 변경
This commit is contained in:
parent
788745ef37
commit
7ccd8fbc6a
|
|
@ -310,8 +310,8 @@ export function CanvasElement({
|
||||||
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
|
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
|
||||||
<span className="text-sm font-bold text-gray-800">{element.title}</span>
|
<span className="text-sm font-bold text-gray-800">{element.title}</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{/* 설정 버튼 */}
|
{/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */}
|
||||||
{onConfigure && (
|
{onConfigure && !(element.type === "widget" && element.subtype === "clock") && (
|
||||||
<button
|
<button
|
||||||
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
|
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
|
||||||
onClick={() => onConfigure(element)}
|
onClick={() => onConfigure(element)}
|
||||||
|
|
@ -354,12 +354,12 @@ export function CanvasElement({
|
||||||
</div>
|
</div>
|
||||||
) : element.type === "widget" && element.subtype === "weather" ? (
|
) : element.type === "widget" && element.subtype === "weather" ? (
|
||||||
// 날씨 위젯 렌더링
|
// 날씨 위젯 렌더링
|
||||||
<div className="h-full w-full widget-interactive-area">
|
<div className="widget-interactive-area h-full w-full">
|
||||||
<WeatherWidget city={element.config?.city || "서울"} refreshInterval={600000} />
|
<WeatherWidget city={element.config?.city || "서울"} refreshInterval={600000} />
|
||||||
</div>
|
</div>
|
||||||
) : element.type === "widget" && element.subtype === "exchange" ? (
|
) : element.type === "widget" && element.subtype === "exchange" ? (
|
||||||
// 환율 위젯 렌더링
|
// 환율 위젯 렌더링
|
||||||
<div className="h-full w-full widget-interactive-area">
|
<div className="widget-interactive-area h-full w-full">
|
||||||
<ExchangeWidget
|
<ExchangeWidget
|
||||||
baseCurrency={element.config?.baseCurrency || "KRW"}
|
baseCurrency={element.config?.baseCurrency || "KRW"}
|
||||||
targetCurrency={element.config?.targetCurrency || "USD"}
|
targetCurrency={element.config?.targetCurrency || "USD"}
|
||||||
|
|
@ -369,7 +369,12 @@ export function CanvasElement({
|
||||||
) : element.type === "widget" && element.subtype === "clock" ? (
|
) : element.type === "widget" && element.subtype === "clock" ? (
|
||||||
// 시계 위젯 렌더링
|
// 시계 위젯 렌더링
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<ClockWidget element={element} />
|
<ClockWidget
|
||||||
|
element={element}
|
||||||
|
onConfigUpdate={(newConfig) => {
|
||||||
|
onUpdate(element.id, { clockConfig: newConfig });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 기타 위젯 렌더링
|
// 기타 위젯 렌더링
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||||
// 모달이 열려있지 않으면 렌더링하지 않음
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
// 시계 위젯인 경우 시계 설정 모달 표시
|
// 시계 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음
|
||||||
if (element.type === "widget" && element.subtype === "clock") {
|
if (element.type === "widget" && element.subtype === "clock") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이전 코드 호환성 유지 (아래 주석 처리된 코드는 제거 예정)
|
||||||
|
if (false && element.type === "widget" && element.subtype === "clock") {
|
||||||
return (
|
return (
|
||||||
<ClockConfigModal
|
<ClockConfigModal
|
||||||
config={
|
config={
|
||||||
|
|
@ -93,7 +98,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
||||||
<div className="flex h-[80vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl">
|
<div className="flex h-[80vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl">
|
||||||
{/* 모달 헤더 */}
|
{/* 모달 헤더 */}
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 p-6">
|
<div className="flex items-center justify-between border-b border-gray-200 p-6">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ClockConfig } from "../types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
interface ClockSettingsProps {
|
||||||
|
config: ClockConfig;
|
||||||
|
onSave: (config: ClockConfig) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시계 위젯 설정 UI (Popover 내부용)
|
||||||
|
* - 모달 없이 순수 설정 폼만 제공
|
||||||
|
*/
|
||||||
|
export function ClockSettings({ config, onSave, onClose }: ClockSettingsProps) {
|
||||||
|
const [localConfig, setLocalConfig] = useState<ClockConfig>(config);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(localConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-[600px] flex-col">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="border-b p-4">
|
||||||
|
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||||
|
<span>⏰</span>
|
||||||
|
시계 설정
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 내용 - 스크롤 가능 */}
|
||||||
|
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||||
|
{/* 스타일 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">시계 스타일</Label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{[
|
||||||
|
{ value: "digital", label: "디지털", icon: "🔢" },
|
||||||
|
{ value: "analog", label: "아날로그", icon: "🕐" },
|
||||||
|
{ value: "both", label: "둘 다", icon: "⏰" },
|
||||||
|
].map((style) => (
|
||||||
|
<Button
|
||||||
|
key={style.value}
|
||||||
|
type="button"
|
||||||
|
variant={localConfig.style === style.value ? "default" : "outline"}
|
||||||
|
onClick={() => setLocalConfig({ ...localConfig, style: style.value as any })}
|
||||||
|
className="flex h-auto flex-col items-center gap-1 py-3"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<span className="text-2xl">{style.icon}</span>
|
||||||
|
<span className="text-xs">{style.label}</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* 타임존 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">타임존</Label>
|
||||||
|
<Select
|
||||||
|
value={localConfig.timezone}
|
||||||
|
onValueChange={(value) => setLocalConfig({ ...localConfig, timezone: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full" size="sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Asia/Seoul">🇰🇷 서울 (KST)</SelectItem>
|
||||||
|
<SelectItem value="Asia/Tokyo">🇯🇵 도쿄 (JST)</SelectItem>
|
||||||
|
<SelectItem value="Asia/Shanghai">🇨🇳 베이징 (CST)</SelectItem>
|
||||||
|
<SelectItem value="America/New_York">🇺🇸 뉴욕 (EST)</SelectItem>
|
||||||
|
<SelectItem value="America/Los_Angeles">🇺🇸 LA (PST)</SelectItem>
|
||||||
|
<SelectItem value="Europe/London">🇬🇧 런던 (GMT)</SelectItem>
|
||||||
|
<SelectItem value="Europe/Paris">🇫🇷 파리 (CET)</SelectItem>
|
||||||
|
<SelectItem value="Australia/Sydney">🇦🇺 시드니 (AEDT)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* 테마 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">테마</Label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
value: "light",
|
||||||
|
label: "Light",
|
||||||
|
gradient: "bg-gradient-to-br from-white to-gray-100",
|
||||||
|
text: "text-gray-900",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "dark",
|
||||||
|
label: "Dark",
|
||||||
|
gradient: "bg-gradient-to-br from-gray-800 to-gray-900",
|
||||||
|
text: "text-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "custom",
|
||||||
|
label: "사용자",
|
||||||
|
gradient: "bg-gradient-to-br from-blue-400 to-purple-600",
|
||||||
|
text: "text-white",
|
||||||
|
},
|
||||||
|
].map((theme) => (
|
||||||
|
<Button
|
||||||
|
key={theme.value}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setLocalConfig({ ...localConfig, theme: theme.value as any })}
|
||||||
|
className={`relative h-auto overflow-hidden p-0 ${
|
||||||
|
localConfig.theme === theme.value ? "ring-primary ring-2 ring-offset-2" : ""
|
||||||
|
}`}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<div className={`${theme.gradient} ${theme.text} w-full rounded px-3 py-2 text-xs font-medium`}>
|
||||||
|
{theme.label}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용자 지정 색상 */}
|
||||||
|
{localConfig.theme === "custom" && (
|
||||||
|
<Card className="mt-2 border p-3">
|
||||||
|
<Label className="mb-2 block text-xs font-medium">배경 색상</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={localConfig.customColor || "#3b82f6"}
|
||||||
|
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||||
|
className="h-10 w-16 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={localConfig.customColor || "#3b82f6"}
|
||||||
|
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||||
|
placeholder="#3b82f6"
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* 옵션 토글 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">표시 옵션</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* 날짜 표시 */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">📅</span>
|
||||||
|
<Label className="cursor-pointer text-sm">날짜 표시</Label>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={localConfig.showDate}
|
||||||
|
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, showDate: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 초 표시 */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">⏱️</span>
|
||||||
|
<Label className="cursor-pointer text-sm">초 표시</Label>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={localConfig.showSeconds}
|
||||||
|
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, showSeconds: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 24시간 형식 */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">🕐</span>
|
||||||
|
<Label className="cursor-pointer text-sm">24시간 형식</Label>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={localConfig.format24h}
|
||||||
|
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, format24h: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="flex justify-end gap-2 border-t p-4">
|
||||||
|
<Button variant="outline" size="sm" onClick={onClose}>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={handleSave}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { DashboardElement } from "../types";
|
import { DashboardElement, ClockConfig } from "../types";
|
||||||
import { AnalogClock } from "./AnalogClock";
|
import { AnalogClock } from "./AnalogClock";
|
||||||
import { DigitalClock } from "./DigitalClock";
|
import { DigitalClock } from "./DigitalClock";
|
||||||
|
import { ClockSettings } from "./ClockSettings";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
interface ClockWidgetProps {
|
interface ClockWidgetProps {
|
||||||
element: DashboardElement;
|
element: DashboardElement;
|
||||||
|
onConfigUpdate?: (config: ClockConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14,9 +19,11 @@ interface ClockWidgetProps {
|
||||||
* - 실시간으로 1초마다 업데이트
|
* - 실시간으로 1초마다 업데이트
|
||||||
* - 아날로그/디지털/둘다 스타일 지원
|
* - 아날로그/디지털/둘다 스타일 지원
|
||||||
* - 타임존 지원
|
* - 타임존 지원
|
||||||
|
* - 내장 설정 UI
|
||||||
*/
|
*/
|
||||||
export function ClockWidget({ element }: ClockWidgetProps) {
|
export function ClockWidget({ element, onConfigUpdate }: ClockWidgetProps) {
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
|
||||||
// 기본 설정값
|
// 기본 설정값
|
||||||
const config = element.clockConfig || {
|
const config = element.clockConfig || {
|
||||||
|
|
@ -26,6 +33,13 @@ export function ClockWidget({ element }: ClockWidgetProps) {
|
||||||
showSeconds: true,
|
showSeconds: true,
|
||||||
format24h: true,
|
format24h: true,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
|
customColor: "#3b82f6",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 설정 저장 핸들러
|
||||||
|
const handleSaveSettings = (newConfig: ClockConfig) => {
|
||||||
|
onConfigUpdate?.(newConfig);
|
||||||
|
setSettingsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1초마다 시간 업데이트
|
// 1초마다 시간 업데이트
|
||||||
|
|
@ -38,23 +52,21 @@ export function ClockWidget({ element }: ClockWidgetProps) {
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 스타일별 렌더링
|
// 시계 콘텐츠 렌더링
|
||||||
|
const renderClockContent = () => {
|
||||||
if (config.style === "analog") {
|
if (config.style === "analog") {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
|
||||||
<AnalogClock
|
<AnalogClock
|
||||||
time={currentTime}
|
time={currentTime}
|
||||||
theme={config.theme}
|
theme={config.theme}
|
||||||
timezone={config.timezone}
|
timezone={config.timezone}
|
||||||
customColor={config.customColor}
|
customColor={config.customColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.style === "digital") {
|
if (config.style === "digital") {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
|
||||||
<DigitalClock
|
<DigitalClock
|
||||||
time={currentTime}
|
time={currentTime}
|
||||||
timezone={config.timezone}
|
timezone={config.timezone}
|
||||||
|
|
@ -64,14 +76,12 @@ export function ClockWidget({ element }: ClockWidgetProps) {
|
||||||
theme={config.theme}
|
theme={config.theme}
|
||||||
customColor={config.customColor}
|
customColor={config.customColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'both' - 아날로그 + 디지털 (작은 크기에 최적화)
|
// 'both' - 아날로그 + 디지털
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
{/* 아날로그 시계 (상단 55%) */}
|
|
||||||
<div className="flex-[55] overflow-hidden">
|
<div className="flex-[55] overflow-hidden">
|
||||||
<AnalogClock
|
<AnalogClock
|
||||||
time={currentTime}
|
time={currentTime}
|
||||||
|
|
@ -80,8 +90,6 @@ export function ClockWidget({ element }: ClockWidgetProps) {
|
||||||
customColor={config.customColor}
|
customColor={config.customColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 디지털 시계 (하단 45%) - 컴팩트 버전 */}
|
|
||||||
<div className="flex-[45] overflow-hidden">
|
<div className="flex-[45] overflow-hidden">
|
||||||
<DigitalClock
|
<DigitalClock
|
||||||
time={currentTime}
|
time={currentTime}
|
||||||
|
|
@ -96,4 +104,26 @@ export function ClockWidget({ element }: ClockWidgetProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
{/* 시계 콘텐츠 */}
|
||||||
|
{renderClockContent()}
|
||||||
|
|
||||||
|
{/* 설정 버튼 - 우측 상단 */}
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[500px] p-0" align="end">
|
||||||
|
<ClockSettings config={config} onSave={handleSaveSettings} onClose={() => setSettingsOpen(false)} />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue