From ce65e6106d611f5b9b9430b8d82b0eb23bc55bad Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 10:12:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EA=B3=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CLOCK_WIDGET_PLAN.md | 34 ++- .../admin/dashboard/ElementConfigModal.tsx | 127 ++++++----- frontend/components/admin/dashboard/types.ts | 3 +- .../admin/dashboard/widgets/AnalogClock.tsx | 104 +++++++-- .../dashboard/widgets/ClockConfigModal.tsx | 205 ++++++++++++++++++ .../admin/dashboard/widgets/ClockWidget.tsx | 31 ++- .../admin/dashboard/widgets/DigitalClock.tsx | 59 +++-- 7 files changed, 448 insertions(+), 115 deletions(-) create mode 100644 frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx diff --git a/frontend/components/admin/dashboard/CLOCK_WIDGET_PLAN.md b/frontend/components/admin/dashboard/CLOCK_WIDGET_PLAN.md index f6f7a1c1..2927fb5b 100644 --- a/frontend/components/admin/dashboard/CLOCK_WIDGET_PLAN.md +++ b/frontend/components/admin/dashboard/CLOCK_WIDGET_PLAN.md @@ -466,10 +466,12 @@ const themes = { ### Step 6: 설정 모달 -- [ ] `ClockConfigModal.tsx` 생성 (향후 추가 예정) -- [ ] 스타일 선택 UI (향후 추가 예정) -- [ ] 타임존 선택 UI (향후 추가 예정) -- [ ] 옵션 토글 UI (향후 추가 예정) +- [x] `ClockConfigModal.tsx` 생성 ✨ +- [x] 스타일 선택 UI (아날로그/디지털/둘다) ✨ +- [x] 타임존 선택 UI (8개 주요 도시) ✨ +- [x] 옵션 토글 UI (날짜/초/24시간) ✨ +- [x] 테마 선택 UI (light/dark/blue/gradient) ✨ +- [x] ElementConfigModal 통합 ✨ ### Step 7: 통합 @@ -547,7 +549,7 @@ const TIMEZONES = [ - [x] 시계가 실시간으로 정확하게 업데이트됨 (1초마다 업데이트) - [x] 아날로그/디지털 스타일 모두 정상 작동 (코드 구현 완료) - [x] 타임존 변경이 즉시 반영됨 (Intl.DateTimeFormat 사용) -- [ ] 설정 모달에서 모든 옵션 변경 가능 (향후 추가) +- [x] 설정 모달에서 모든 옵션 변경 가능 ✨ (ClockConfigModal 완성!) - [x] 테마 전환이 자연스러움 (4가지 테마 구현) - [x] 메모리 누수 없음 (컴포넌트 unmount 시 타이머 정리 - useEffect cleanup) - [x] 크기 조절 시 레이아웃이 깨지지 않음 (그리드 스냅 적용) @@ -603,13 +605,31 @@ console.log(formatter.format(new Date())); // "05:30" 5. **통합** - CanvasElement, DashboardDesigner, Sidebar 연동 6. **테마** - light, dark, blue, gradient 4가지 테마 +### ✅ 최종 완료 기능 + +1. **시계 위젯 컴포넌트** - 아날로그/디지털/둘다 +2. **실시간 업데이트** - 1초마다 정확한 시간 +3. **타임존 지원** - 8개 주요 도시 +4. **4가지 테마** - light, dark, blue, gradient +5. **설정 모달** - 모든 옵션 UI로 변경 가능 ✨ + ### 🔜 향후 추가 예정 -- 설정 모달 (스타일, 타임존, 옵션 변경 UI) - 세계 시계 (여러 타임존 동시 표시) - 알람 기능 - 타이머/스톱워치 +- 커스텀 색상 선택 --- -이제 대시보드에서 시계 위젯을 드래그해서 사용할 수 있습니다! 🚀⏰ +## 🎯 사용 방법 + +1. **시계 추가**: 우측 사이드바에서 "⏰ 시계 위젯" 드래그 +2. **설정 변경**: 시계 위에 마우스 올리고 ⚙️ 버튼 클릭 +3. **옵션 선택**: + - 스타일 (디지털/아날로그/둘다) + - 타임존 (서울, 뉴욕, 런던 등) + - 테마 (4가지) + - 날짜/초/24시간 형식 + +이제 완벽하게 작동하는 시계 위젯을 사용할 수 있습니다! 🚀⏰ diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index 4155a00a..5dc82900 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -1,9 +1,10 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from './types'; -import { QueryEditor } from './QueryEditor'; -import { ChartConfigPanel } from './ChartConfigPanel'; +import React, { useState, useCallback } from "react"; +import { DashboardElement, ChartDataSource, ChartConfig, QueryResult, ClockConfig } from "./types"; +import { QueryEditor } from "./QueryEditor"; +import { ChartConfigPanel } from "./ChartConfigPanel"; +import { ClockConfigModal } from "./widgets/ClockConfigModal"; interface ElementConfigModalProps { element: DashboardElement; @@ -20,13 +21,11 @@ interface ElementConfigModalProps { */ export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) { const [dataSource, setDataSource] = useState( - element.dataSource || { type: 'database', refreshInterval: 30000 } - ); - const [chartConfig, setChartConfig] = useState( - element.chartConfig || {} + element.dataSource || { type: "database", refreshInterval: 30000 }, ); + const [chartConfig, setChartConfig] = useState(element.chartConfig || {}); const [queryResult, setQueryResult] = useState(null); - const [activeTab, setActiveTab] = useState<'query' | 'chart'>('query'); + const [activeTab, setActiveTab] = useState<"query" | "chart">("query"); // 데이터 소스 변경 처리 const handleDataSourceChange = useCallback((newDataSource: ChartDataSource) => { @@ -43,7 +42,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element setQueryResult(result); // 쿼리 결과가 나오면 자동으로 차트 설정 탭으로 이동 if (result.rows.length > 0) { - setActiveTab('chart'); + setActiveTab("chart"); } }, []); @@ -58,26 +57,51 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element onClose(); }, [element, dataSource, chartConfig, onSave, onClose]); + // 시계 위젯 설정 저장 + const handleClockConfigSave = useCallback( + (clockConfig: ClockConfig) => { + const updatedElement: DashboardElement = { + ...element, + clockConfig, + }; + onSave(updatedElement); + }, + [element, onSave], + ); + // 모달이 열려있지 않으면 렌더링하지 않음 if (!isOpen) return null; + // 시계 위젯인 경우 시계 설정 모달 표시 + if (element.type === "widget" && element.subtype === "clock") { + return ( + + ); + } + return ( -
-
+
+
{/* 모달 헤더 */} -
+
-

- {element.title} 설정 -

-

- 데이터 소스와 차트 설정을 구성하세요 -

+

{element.title} 설정

+

데이터 소스와 차트 설정을 구성하세요

-
@@ -85,28 +109,26 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element {/* 탭 네비게이션 */}
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 8c78727f..d304c9f3 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -83,5 +83,6 @@ export interface ClockConfig { showDate: boolean; // 날짜 표시 여부 showSeconds: boolean; // 초 표시 여부 (디지털) format24h: boolean; // 24시간 형식 (true) vs 12시간 형식 (false) - theme: "light" | "dark" | "blue" | "gradient"; // 테마 + theme: "light" | "dark" | "custom"; // 테마 + customColor?: string; // 사용자 지정 색상 (custom 테마일 때) } diff --git a/frontend/components/admin/dashboard/widgets/AnalogClock.tsx b/frontend/components/admin/dashboard/widgets/AnalogClock.tsx index 44699a15..52131695 100644 --- a/frontend/components/admin/dashboard/widgets/AnalogClock.tsx +++ b/frontend/components/admin/dashboard/widgets/AnalogClock.tsx @@ -2,7 +2,9 @@ interface AnalogClockProps { time: Date; - theme: "light" | "dark" | "blue" | "gradient"; + theme: "light" | "dark" | "custom"; + timezone?: string; + customColor?: string; // 사용자 지정 색상 } /** @@ -10,8 +12,9 @@ interface AnalogClockProps { * - SVG 기반 아날로그 시계 * - 시침, 분침, 초침 애니메이션 * - 테마별 색상 지원 + * - 타임존 표시 */ -export function AnalogClock({ time, theme }: AnalogClockProps) { +export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockProps) { const hours = time.getHours() % 12; const minutes = time.getMinutes(); const seconds = time.getSeconds(); @@ -22,11 +25,14 @@ export function AnalogClock({ time, theme }: AnalogClockProps) { const hourAngle = hours * 30 + minutes * 0.5 - 90; // 30도씩 + 분당 0.5도 // 테마별 색상 - const colors = getThemeColors(theme); + const colors = getThemeColors(theme, customColor); + + // 타임존 라벨 + const timezoneLabel = timezone ? getTimezoneLabel(timezone) : ""; return ( -
- +
+ {/* 시계판 배경 */} @@ -110,14 +116,56 @@ export function AnalogClock({ time, theme }: AnalogClockProps) { + + {/* 타임존 표시 */} + {timezoneLabel && ( +
+ {timezoneLabel} +
+ )}
); } +/** + * 타임존 라벨 반환 + */ +function getTimezoneLabel(timezone: string): string { + const timezoneLabels: Record = { + "Asia/Seoul": "서울 (KST)", + "Asia/Tokyo": "도쿄 (JST)", + "Asia/Shanghai": "베이징 (CST)", + "America/New_York": "뉴욕 (EST)", + "America/Los_Angeles": "LA (PST)", + "Europe/London": "런던 (GMT)", + "Europe/Paris": "파리 (CET)", + "Australia/Sydney": "시드니 (AEDT)", + }; + + return timezoneLabels[timezone] || timezone.split("/")[1]; +} + /** * 테마별 색상 반환 */ -function getThemeColors(theme: string) { +function getThemeColors(theme: string, customColor?: string) { + if (theme === "custom" && customColor) { + // 사용자 지정 색상 사용 (약간 밝게/어둡게 조정) + const lighterColor = adjustColor(customColor, 40); + const darkerColor = adjustColor(customColor, -40); + + return { + background: lighterColor, + border: customColor, + tick: customColor, + number: darkerColor, + hourHand: darkerColor, + minuteHand: customColor, + secondHand: "#ef4444", + center: darkerColor, + }; + } + const themes = { light: { background: "#ffffff", @@ -139,27 +187,35 @@ function getThemeColors(theme: string) { secondHand: "#ef4444", center: "#f9fafb", }, - blue: { - background: "#dbeafe", - border: "#3b82f6", - tick: "#60a5fa", - number: "#1e40af", - hourHand: "#1e3a8a", - minuteHand: "#2563eb", + custom: { + background: "#e0e7ff", + border: "#6366f1", + tick: "#818cf8", + number: "#4338ca", + hourHand: "#4338ca", + minuteHand: "#6366f1", secondHand: "#ef4444", - center: "#1e3a8a", - }, - gradient: { - background: "#fce7f3", - border: "#ec4899", - tick: "#f472b6", - number: "#9333ea", - hourHand: "#7c3aed", - minuteHand: "#a855f7", - secondHand: "#ef4444", - center: "#7c3aed", + center: "#4338ca", }, }; return themes[theme as keyof typeof themes] || themes.light; } + +/** + * 색상 밝기 조정 + */ +function adjustColor(color: string, amount: number): string { + const clamp = (num: number) => Math.min(255, Math.max(0, num)); + + const hex = color.replace("#", ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + const newR = clamp(r + amount); + const newG = clamp(g + amount); + const newB = clamp(b + amount); + + return `#${newR.toString(16).padStart(2, "0")}${newG.toString(16).padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`; +} diff --git a/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx b/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx new file mode 100644 index 00000000..26067b48 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { useState } from "react"; +import { ClockConfig } from "../types"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +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 { X } from "lucide-react"; + +interface ClockConfigModalProps { + config: ClockConfig; + onSave: (config: ClockConfig) => void; + onClose: () => void; +} + +/** + * 시계 위젯 설정 모달 + * - 스타일 선택 (아날로그/디지털/둘다) + * - 타임존 선택 + * - 테마 선택 + * - 옵션 토글 (날짜, 초, 24시간) + */ +export function ClockConfigModal({ config, onSave, onClose }: ClockConfigModalProps) { + const [localConfig, setLocalConfig] = useState(config); + + const handleSave = () => { + onSave(localConfig); + onClose(); + }; + + return ( + + + + + + 시계 위젯 설정 + + + + {/* 내용 - 스크롤 가능 */} +
+ {/* 스타일 선택 */} +
+ +
+ {[ + { value: "digital", label: "디지털", icon: "🔢" }, + { value: "analog", label: "아날로그", icon: "🕐" }, + { value: "both", label: "둘 다", icon: "⏰" }, + ].map((style) => ( + + ))} +
+
+ + {/* 타임존 선택 */} +
+ + +
+ + {/* 테마 선택 */} +
+ +
+ {[ + { + 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) => ( + + ))} +
+ + {/* 사용자 지정 색상 선택 */} + {localConfig.theme === "custom" && ( + + +
+ setLocalConfig({ ...localConfig, customColor: e.target.value })} + className="h-12 w-20 cursor-pointer" + /> +
+ setLocalConfig({ ...localConfig, customColor: e.target.value })} + placeholder="#3b82f6" + className="font-mono" + /> +

시계의 배경색이나 강조색으로 사용됩니다

+
+
+
+ )} +
+ + {/* 옵션 토글 */} +
+ +
+ {/* 날짜 표시 */} + + 📅 + + setLocalConfig({ ...localConfig, showDate: checked })} + /> + + + {/* 초 표시 */} + + ⏱️ + + setLocalConfig({ ...localConfig, showSeconds: checked })} + /> + + + {/* 24시간 형식 */} + + 🕐 + + setLocalConfig({ ...localConfig, format24h: checked })} + /> + +
+
+
+ + + + + +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/widgets/ClockWidget.tsx b/frontend/components/admin/dashboard/widgets/ClockWidget.tsx index ad748d5a..cd0661db 100644 --- a/frontend/components/admin/dashboard/widgets/ClockWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/ClockWidget.tsx @@ -42,7 +42,12 @@ export function ClockWidget({ element }: ClockWidgetProps) { if (config.style === "analog") { return (
- +
); } @@ -57,28 +62,36 @@ export function ClockWidget({ element }: ClockWidgetProps) { showSeconds={config.showSeconds} format24h={config.format24h} theme={config.theme} + customColor={config.customColor} />
); } - // 'both' - 아날로그 + 디지털 + // 'both' - 아날로그 + 디지털 (작은 크기에 최적화) return ( -
- {/* 아날로그 시계 (상단 60%) */} -
- +
+ {/* 아날로그 시계 (상단 55%) */} +
+
- {/* 디지털 시계 (하단 40%) */} -
+ {/* 디지털 시계 (하단 45%) - 컴팩트 버전 */} +
diff --git a/frontend/components/admin/dashboard/widgets/DigitalClock.tsx b/frontend/components/admin/dashboard/widgets/DigitalClock.tsx index b168e22e..eb8b9cba 100644 --- a/frontend/components/admin/dashboard/widgets/DigitalClock.tsx +++ b/frontend/components/admin/dashboard/widgets/DigitalClock.tsx @@ -6,7 +6,9 @@ interface DigitalClockProps { showDate: boolean; showSeconds: boolean; format24h: boolean; - theme: "light" | "dark" | "blue" | "gradient"; + theme: "light" | "dark" | "custom"; + compact?: boolean; // 작은 크기에서 사용 + customColor?: string; // 사용자 지정 색상 } /** @@ -16,7 +18,16 @@ interface DigitalClockProps { * - 날짜/초 표시 옵션 * - 12/24시간 형식 지원 */ -export function DigitalClock({ time, timezone, showDate, showSeconds, format24h, theme }: DigitalClockProps) { +export function DigitalClock({ + time, + timezone, + showDate, + showSeconds, + format24h, + theme, + compact = false, + customColor, +}: DigitalClockProps) { // 시간 포맷팅 (타임존 적용) const timeString = new Intl.DateTimeFormat("ko-KR", { timeZone: timezone, @@ -41,18 +52,27 @@ export function DigitalClock({ time, timezone, showDate, showSeconds, format24h, const timezoneLabel = getTimezoneLabel(timezone); // 테마별 스타일 - const themeClasses = getThemeClasses(theme); + const themeClasses = getThemeClasses(theme, customColor); return ( -
- {/* 날짜 표시 */} - {showDate && dateString &&
{dateString}
} +
+ {/* 날짜 표시 (compact 모드에서는 숨김) */} + {!compact && showDate && dateString && ( +
{dateString}
+ )} {/* 시간 표시 */} -
{timeString}
+
+ {timeString} +
{/* 타임존 표시 */} -
{timezoneLabel}
+
+ {timezoneLabel} +
); } @@ -78,7 +98,18 @@ function getTimezoneLabel(timezone: string): string { /** * 테마별 클래스 반환 */ -function getThemeClasses(theme: string) { +function getThemeClasses(theme: string, customColor?: string) { + if (theme === "custom" && customColor) { + // 사용자 지정 색상 사용 + return { + container: "text-white", + date: "text-white/80", + time: "text-white", + timezone: "text-white/70", + style: { backgroundColor: customColor }, + }; + } + const themes = { light: { container: "bg-white text-gray-900", @@ -92,18 +123,12 @@ function getThemeClasses(theme: string) { time: "text-white", timezone: "text-gray-400", }, - blue: { - container: "bg-gradient-to-br from-blue-400 to-blue-600 text-white", + custom: { + container: "bg-gradient-to-br from-blue-400 to-purple-600 text-white", date: "text-blue-100", time: "text-white", timezone: "text-blue-200", }, - gradient: { - container: "bg-gradient-to-br from-purple-400 via-pink-500 to-red-500 text-white", - date: "text-purple-100", - time: "text-white", - timezone: "text-pink-200", - }, }; return themes[theme as keyof typeof themes] || themes.light;