- {/* 설정 버튼 */}
- {onConfigure && (
+ {/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */}
+ {onConfigure && !(element.type === "widget" && element.subtype === "clock") && (
) : element.type === "widget" && element.subtype === "weather" ? (
// 날씨 위젯 렌더링
-
+
) : element.type === "widget" && element.subtype === "exchange" ? (
// 환율 위젯 렌더링
-
+
+ ) : element.type === "widget" && element.subtype === "clock" ? (
+ // 시계 위젯 렌더링
+
+ {
+ onUpdate(element.id, { clockConfig: newConfig });
+ }}
+ />
+
) : (
// 기타 위젯 렌더링
{/* 편집 중인 대시보드 표시 */}
{dashboardTitle && (
-
+
📝 편집 중: {dashboardTitle}
)}
@@ -289,6 +289,8 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
return "💱 환율 위젯";
case "weather":
return "☁️ 날씨 위젯";
+ case "clock":
+ return "⏰ 시계 위젯";
default:
return "🔧 위젯";
}
@@ -315,6 +317,8 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string {
return "USD: ₩1,320\nJPY: ₩900\nEUR: ₩1,450";
case "weather":
return "서울\n23°C\n구름 많음";
+ case "clock":
+ return "clock";
default:
return "위젯 내용이 여기에 표시됩니다";
}
diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx
index 6ff1502c..41888172 100644
--- a/frontend/components/admin/dashboard/DashboardSidebar.tsx
+++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx
@@ -103,6 +103,14 @@ export function DashboardSidebar() {
onDragStart={handleDragStart}
className="border-l-4 border-orange-500"
/>
+
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx
index 4155a00a..dc9d3f32 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,56 @@ 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;
+ // 시계 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음
+ if (element.type === "widget" && element.subtype === "clock") {
+ return null;
+ }
+
+ // 이전 코드 호환성 유지 (아래 주석 처리된 코드는 제거 예정)
+ if (false && element.type === "widget" && element.subtype === "clock") {
+ return (
+
+ );
+ }
+
return (
-
-
+
+
{/* 모달 헤더 */}
-
+
-
- {element.title} 설정
-
-
- 데이터 소스와 차트 설정을 구성하세요
-
+
{element.title} 설정
+
데이터 소스와 차트 설정을 구성하세요
-
@@ -85,28 +114,26 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
{/* 탭 네비게이션 */}
setActiveTab('query')}
- className={`
- px-6 py-3 text-sm font-medium border-b-2 transition-colors
- ${activeTab === 'query'
- ? 'border-primary text-primary bg-accent'
- : 'border-transparent text-gray-500 hover:text-gray-700'}
- `}
+ onClick={() => setActiveTab("query")}
+ className={`border-b-2 px-6 py-3 text-sm font-medium transition-colors ${
+ activeTab === "query"
+ ? "border-primary text-primary bg-accent"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ } `}
>
📝 쿼리 & 데이터
setActiveTab('chart')}
- className={`
- px-6 py-3 text-sm font-medium border-b-2 transition-colors
- ${activeTab === 'chart'
- ? 'border-primary text-primary bg-accent'
- : 'border-transparent text-gray-500 hover:text-gray-700'}
- `}
+ onClick={() => setActiveTab("chart")}
+ className={`border-b-2 px-6 py-3 text-sm font-medium transition-colors ${
+ activeTab === "chart"
+ ? "border-primary text-primary bg-accent"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ } `}
>
📊 차트 설정
{queryResult && (
-
+
{queryResult.rows.length}
)}
@@ -115,7 +142,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
{/* 탭 내용 */}
- {activeTab === 'query' && (
+ {activeTab === "query" && (
)}
- {activeTab === 'chart' && (
-
+ {activeTab === "chart" && (
+
)}
{/* 모달 푸터 */}
-
+
{dataSource.query && (
<>
- 💾 쿼리: {dataSource.query.length > 50
- ? `${dataSource.query.substring(0, 50)}...`
- : dataSource.query}
+ 💾 쿼리: {dataSource.query.length > 50 ? `${dataSource.query.substring(0, 50)}...` : dataSource.query}
>
)}
-
+
취소
저장
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts
index ab2ec13e..d304c9f3 100644
--- a/frontend/components/admin/dashboard/types.ts
+++ b/frontend/components/admin/dashboard/types.ts
@@ -2,11 +2,19 @@
* 대시보드 관리 시스템 타입 정의
*/
-export type ElementType = 'chart' | 'widget';
+export type ElementType = "chart" | "widget";
-export type ElementSubtype =
- | 'bar' | 'pie' | 'line' | 'area' | 'stacked-bar' | 'donut' | 'combo' // 차트 타입
- | 'exchange' | 'weather'; // 위젯 타입
+export type ElementSubtype =
+ | "bar"
+ | "pie"
+ | "line"
+ | "area"
+ | "stacked-bar"
+ | "donut"
+ | "combo" // 차트 타입
+ | "exchange"
+ | "weather"
+ | "clock"; // 위젯 타입
export interface Position {
x: number;
@@ -26,8 +34,9 @@ export interface DashboardElement {
size: Size;
title: string;
content: string;
- dataSource?: ChartDataSource; // 데이터 소스 설정
- chartConfig?: ChartConfig; // 차트 설정
+ dataSource?: ChartDataSource; // 데이터 소스 설정
+ chartConfig?: ChartConfig; // 차트 설정
+ clockConfig?: ClockConfig; // 시계 설정
}
export interface DragData {
@@ -36,33 +45,44 @@ export interface DragData {
}
export interface ResizeHandle {
- direction: 'nw' | 'ne' | 'sw' | 'se';
+ direction: "nw" | "ne" | "sw" | "se";
cursor: string;
}
export interface ChartDataSource {
- type: 'api' | 'database' | 'static';
- endpoint?: string; // API 엔드포인트
- query?: string; // SQL 쿼리
+ type: "api" | "database" | "static";
+ endpoint?: string; // API 엔드포인트
+ query?: string; // SQL 쿼리
refreshInterval?: number; // 자동 새로고침 간격 (ms)
- filters?: any[]; // 필터 조건
- lastExecuted?: string; // 마지막 실행 시간
+ filters?: any[]; // 필터 조건
+ lastExecuted?: string; // 마지막 실행 시간
}
export interface ChartConfig {
- xAxis?: string; // X축 데이터 필드
- yAxis?: string | string[]; // Y축 데이터 필드 (단일 또는 다중)
- groupBy?: string; // 그룹핑 필드
- aggregation?: 'sum' | 'avg' | 'count' | 'max' | 'min';
- colors?: string[]; // 차트 색상
- title?: string; // 차트 제목
- showLegend?: boolean; // 범례 표시 여부
+ xAxis?: string; // X축 데이터 필드
+ yAxis?: string | string[]; // Y축 데이터 필드 (단일 또는 다중)
+ groupBy?: string; // 그룹핑 필드
+ aggregation?: "sum" | "avg" | "count" | "max" | "min";
+ colors?: string[]; // 차트 색상
+ title?: string; // 차트 제목
+ showLegend?: boolean; // 범례 표시 여부
}
export interface QueryResult {
- columns: string[]; // 컬럼명 배열
+ columns: string[]; // 컬럼명 배열
rows: Record
[]; // 데이터 행 배열
- totalRows: number; // 전체 행 수
- executionTime: number; // 실행 시간 (ms)
- error?: string; // 오류 메시지
+ totalRows: number; // 전체 행 수
+ executionTime: number; // 실행 시간 (ms)
+ error?: string; // 오류 메시지
+}
+
+// 시계 위젯 설정
+export interface ClockConfig {
+ style: "analog" | "digital" | "both"; // 시계 스타일
+ timezone: string; // 타임존 (예: 'Asia/Seoul')
+ showDate: boolean; // 날짜 표시 여부
+ showSeconds: boolean; // 초 표시 여부 (디지털)
+ format24h: boolean; // 24시간 형식 (true) vs 12시간 형식 (false)
+ 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
new file mode 100644
index 00000000..52131695
--- /dev/null
+++ b/frontend/components/admin/dashboard/widgets/AnalogClock.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+interface AnalogClockProps {
+ time: Date;
+ theme: "light" | "dark" | "custom";
+ timezone?: string;
+ customColor?: string; // 사용자 지정 색상
+}
+
+/**
+ * 아날로그 시계 컴포넌트
+ * - SVG 기반 아날로그 시계
+ * - 시침, 분침, 초침 애니메이션
+ * - 테마별 색상 지원
+ * - 타임존 표시
+ */
+export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockProps) {
+ const hours = time.getHours() % 12;
+ const minutes = time.getMinutes();
+ const seconds = time.getSeconds();
+
+ // 각도 계산 (12시 방향을 0도로, 시계방향으로 회전)
+ const secondAngle = seconds * 6 - 90; // 6도씩 회전 (360/60)
+ const minuteAngle = minutes * 6 + seconds * 0.1 - 90; // 6도씩 + 초당 0.1도
+ const hourAngle = hours * 30 + minutes * 0.5 - 90; // 30도씩 + 분당 0.5도
+
+ // 테마별 색상
+ const colors = getThemeColors(theme, customColor);
+
+ // 타임존 라벨
+ const timezoneLabel = timezone ? getTimezoneLabel(timezone) : "";
+
+ return (
+
+
+
+ {/* 타임존 표시 */}
+ {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, 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",
+ border: "#d1d5db",
+ tick: "#9ca3af",
+ number: "#374151",
+ hourHand: "#1f2937",
+ minuteHand: "#4b5563",
+ secondHand: "#ef4444",
+ center: "#1f2937",
+ },
+ dark: {
+ background: "#1f2937",
+ border: "#4b5563",
+ tick: "#6b7280",
+ number: "#f9fafb",
+ hourHand: "#f9fafb",
+ minuteHand: "#d1d5db",
+ secondHand: "#ef4444",
+ center: "#f9fafb",
+ },
+ custom: {
+ background: "#e0e7ff",
+ border: "#6366f1",
+ tick: "#818cf8",
+ number: "#4338ca",
+ hourHand: "#4338ca",
+ minuteHand: "#6366f1",
+ secondHand: "#ef4444",
+ 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 (
+
+ );
+}
diff --git a/frontend/components/admin/dashboard/widgets/ClockSettings.tsx b/frontend/components/admin/dashboard/widgets/ClockSettings.tsx
new file mode 100644
index 00000000..dd28c3af
--- /dev/null
+++ b/frontend/components/admin/dashboard/widgets/ClockSettings.tsx
@@ -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(config);
+
+ const handleSave = () => {
+ onSave(localConfig);
+ };
+
+ return (
+
+ {/* 헤더 */}
+
+
+ ⏰
+ 시계 설정
+
+
+
+ {/* 내용 - 스크롤 가능 */}
+
+ {/* 스타일 선택 */}
+
+
+
+ {[
+ { value: "digital", label: "디지털", icon: "🔢" },
+ { value: "analog", label: "아날로그", icon: "🕐" },
+ { value: "both", label: "둘 다", icon: "⏰" },
+ ].map((style) => (
+ setLocalConfig({ ...localConfig, style: style.value as any })}
+ className="flex h-auto flex-col items-center gap-1 py-3"
+ size="sm"
+ >
+ {style.icon}
+ {style.label}
+
+ ))}
+
+
+
+
+
+ {/* 타임존 선택 */}
+
+
+
+
+
+
+
+ {/* 테마 선택 */}
+
+
+
+ {[
+ {
+ 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) => (
+
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"
+ >
+
+ {theme.label}
+
+
+ ))}
+
+
+ {/* 사용자 지정 색상 */}
+ {localConfig.theme === "custom" && (
+
+
+
+ setLocalConfig({ ...localConfig, customColor: e.target.value })}
+ className="h-10 w-16 cursor-pointer"
+ />
+ setLocalConfig({ ...localConfig, customColor: e.target.value })}
+ placeholder="#3b82f6"
+ className="flex-1 font-mono text-xs"
+ />
+
+
+ )}
+
+
+
+
+ {/* 옵션 토글 */}
+
+
+
+ {/* 날짜 표시 */}
+
+
+ 📅
+
+
+
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
new file mode 100644
index 00000000..e85623f8
--- /dev/null
+++ b/frontend/components/admin/dashboard/widgets/ClockWidget.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { DashboardElement, ClockConfig } from "../types";
+import { AnalogClock } from "./AnalogClock";
+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 {
+ element: DashboardElement;
+ onConfigUpdate?: (config: ClockConfig) => void;
+}
+
+/**
+ * 시계 위젯 메인 컴포넌트
+ * - 실시간으로 1초마다 업데이트
+ * - 아날로그/디지털/둘다 스타일 지원
+ * - 타임존 지원
+ * - 내장 설정 UI
+ */
+export function ClockWidget({ element, onConfigUpdate }: ClockWidgetProps) {
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [settingsOpen, setSettingsOpen] = useState(false);
+
+ // 기본 설정값
+ const config = element.clockConfig || {
+ style: "digital",
+ timezone: "Asia/Seoul",
+ showDate: true,
+ showSeconds: true,
+ format24h: true,
+ theme: "light",
+ customColor: "#3b82f6",
+ };
+
+ // 설정 저장 핸들러
+ const handleSaveSettings = (newConfig: ClockConfig) => {
+ onConfigUpdate?.(newConfig);
+ setSettingsOpen(false);
+ };
+
+ // 1초마다 시간 업데이트
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000);
+
+ // cleanup: 컴포넌트 unmount 시 타이머 정리
+ return () => clearInterval(timer);
+ }, []);
+
+ // 시계 콘텐츠 렌더링
+ const renderClockContent = () => {
+ if (config.style === "analog") {
+ return (
+
+ );
+ }
+
+ if (config.style === "digital") {
+ return (
+
+ );
+ }
+
+ // 'both' - 아날로그 + 디지털
+ return (
+
+ );
+ };
+
+ return (
+
+ {/* 시계 콘텐츠 */}
+ {renderClockContent()}
+
+ {/* 설정 버튼 - 우측 상단 */}
+
+
+
+
+
+
+
+
+ setSettingsOpen(false)} />
+
+
+
+
+ );
+}
diff --git a/frontend/components/admin/dashboard/widgets/DigitalClock.tsx b/frontend/components/admin/dashboard/widgets/DigitalClock.tsx
new file mode 100644
index 00000000..eb8b9cba
--- /dev/null
+++ b/frontend/components/admin/dashboard/widgets/DigitalClock.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+interface DigitalClockProps {
+ time: Date;
+ timezone: string;
+ showDate: boolean;
+ showSeconds: boolean;
+ format24h: boolean;
+ theme: "light" | "dark" | "custom";
+ compact?: boolean; // 작은 크기에서 사용
+ customColor?: string; // 사용자 지정 색상
+}
+
+/**
+ * 디지털 시계 컴포넌트
+ * - 실시간 시간 표시
+ * - 타임존 지원
+ * - 날짜/초 표시 옵션
+ * - 12/24시간 형식 지원
+ */
+export function DigitalClock({
+ time,
+ timezone,
+ showDate,
+ showSeconds,
+ format24h,
+ theme,
+ compact = false,
+ customColor,
+}: DigitalClockProps) {
+ // 시간 포맷팅 (타임존 적용)
+ const timeString = new Intl.DateTimeFormat("ko-KR", {
+ timeZone: timezone,
+ hour: "2-digit",
+ minute: "2-digit",
+ second: showSeconds ? "2-digit" : undefined,
+ hour12: !format24h,
+ }).format(time);
+
+ // 날짜 포맷팅 (타임존 적용)
+ const dateString = showDate
+ ? new Intl.DateTimeFormat("ko-KR", {
+ timeZone: timezone,
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ weekday: "long",
+ }).format(time)
+ : null;
+
+ // 타임존 라벨
+ const timezoneLabel = getTimezoneLabel(timezone);
+
+ // 테마별 스타일
+ const themeClasses = getThemeClasses(theme, customColor);
+
+ return (
+
+ {/* 날짜 표시 (compact 모드에서는 숨김) */}
+ {!compact && showDate && dateString && (
+
{dateString}
+ )}
+
+ {/* 시간 표시 */}
+
+ {timeString}
+
+
+ {/* 타임존 표시 */}
+
+ {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;
+}
+
+/**
+ * 테마별 클래스 반환
+ */
+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",
+ date: "text-gray-600",
+ time: "text-gray-900",
+ timezone: "text-gray-500",
+ },
+ dark: {
+ container: "bg-gray-900 text-white",
+ date: "text-gray-300",
+ time: "text-white",
+ timezone: "text-gray-400",
+ },
+ 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",
+ },
+ };
+
+ return themes[theme as keyof typeof themes] || themes.light;
+}