기본 시간(서울) 시계 위젯 구현
This commit is contained in:
165
frontend/components/admin/dashboard/widgets/AnalogClock.tsx
Normal file
165
frontend/components/admin/dashboard/widgets/AnalogClock.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
interface AnalogClockProps {
|
||||
time: Date;
|
||||
theme: "light" | "dark" | "blue" | "gradient";
|
||||
}
|
||||
|
||||
/**
|
||||
* 아날로그 시계 컴포넌트
|
||||
* - SVG 기반 아날로그 시계
|
||||
* - 시침, 분침, 초침 애니메이션
|
||||
* - 테마별 색상 지원
|
||||
*/
|
||||
export function AnalogClock({ time, theme }: 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);
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
<svg viewBox="0 0 200 200" className="h-full max-h-[250px] w-full max-w-[250px]">
|
||||
{/* 시계판 배경 */}
|
||||
<circle cx="100" cy="100" r="98" fill={colors.background} stroke={colors.border} strokeWidth="2" />
|
||||
|
||||
{/* 눈금 표시 */}
|
||||
{[...Array(60)].map((_, i) => {
|
||||
const angle = (i * 6 - 90) * (Math.PI / 180);
|
||||
const isHour = i % 5 === 0;
|
||||
const startRadius = isHour ? 85 : 90;
|
||||
const endRadius = 95;
|
||||
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={100 + startRadius * Math.cos(angle)}
|
||||
y1={100 + startRadius * Math.sin(angle)}
|
||||
x2={100 + endRadius * Math.cos(angle)}
|
||||
y2={100 + endRadius * Math.sin(angle)}
|
||||
stroke={colors.tick}
|
||||
strokeWidth={isHour ? 2 : 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 숫자 표시 (12시, 3시, 6시, 9시) */}
|
||||
{[12, 3, 6, 9].map((num, idx) => {
|
||||
const angle = (idx * 90 - 90) * (Math.PI / 180);
|
||||
const radius = 70;
|
||||
const x = 100 + radius * Math.cos(angle);
|
||||
const y = 100 + radius * Math.sin(angle);
|
||||
|
||||
return (
|
||||
<text
|
||||
key={num}
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize="20"
|
||||
fontWeight="bold"
|
||||
fill={colors.number}
|
||||
>
|
||||
{num}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 시침 (짧고 굵음) */}
|
||||
<line
|
||||
x1="100"
|
||||
y1="100"
|
||||
x2={100 + 40 * Math.cos((hourAngle * Math.PI) / 180)}
|
||||
y2={100 + 40 * Math.sin((hourAngle * Math.PI) / 180)}
|
||||
stroke={colors.hourHand}
|
||||
strokeWidth="6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* 분침 (중간 길이) */}
|
||||
<line
|
||||
x1="100"
|
||||
y1="100"
|
||||
x2={100 + 60 * Math.cos((minuteAngle * Math.PI) / 180)}
|
||||
y2={100 + 60 * Math.sin((minuteAngle * Math.PI) / 180)}
|
||||
stroke={colors.minuteHand}
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* 초침 (가늘고 긴) */}
|
||||
<line
|
||||
x1="100"
|
||||
y1="100"
|
||||
x2={100 + 75 * Math.cos((secondAngle * Math.PI) / 180)}
|
||||
y2={100 + 75 * Math.sin((secondAngle * Math.PI) / 180)}
|
||||
stroke={colors.secondHand}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* 중심점 */}
|
||||
<circle cx="100" cy="100" r="6" fill={colors.center} />
|
||||
<circle cx="100" cy="100" r="3" fill={colors.background} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테마별 색상 반환
|
||||
*/
|
||||
function getThemeColors(theme: string) {
|
||||
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",
|
||||
},
|
||||
blue: {
|
||||
background: "#dbeafe",
|
||||
border: "#3b82f6",
|
||||
tick: "#60a5fa",
|
||||
number: "#1e40af",
|
||||
hourHand: "#1e3a8a",
|
||||
minuteHand: "#2563eb",
|
||||
secondHand: "#ef4444",
|
||||
center: "#1e3a8a",
|
||||
},
|
||||
gradient: {
|
||||
background: "#fce7f3",
|
||||
border: "#ec4899",
|
||||
tick: "#f472b6",
|
||||
number: "#9333ea",
|
||||
hourHand: "#7c3aed",
|
||||
minuteHand: "#a855f7",
|
||||
secondHand: "#ef4444",
|
||||
center: "#7c3aed",
|
||||
},
|
||||
};
|
||||
|
||||
return themes[theme as keyof typeof themes] || themes.light;
|
||||
}
|
||||
86
frontend/components/admin/dashboard/widgets/ClockWidget.tsx
Normal file
86
frontend/components/admin/dashboard/widgets/ClockWidget.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardElement } from "../types";
|
||||
import { AnalogClock } from "./AnalogClock";
|
||||
import { DigitalClock } from "./DigitalClock";
|
||||
|
||||
interface ClockWidgetProps {
|
||||
element: DashboardElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시계 위젯 메인 컴포넌트
|
||||
* - 실시간으로 1초마다 업데이트
|
||||
* - 아날로그/디지털/둘다 스타일 지원
|
||||
* - 타임존 지원
|
||||
*/
|
||||
export function ClockWidget({ element }: ClockWidgetProps) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
|
||||
// 기본 설정값
|
||||
const config = element.clockConfig || {
|
||||
style: "digital",
|
||||
timezone: "Asia/Seoul",
|
||||
showDate: true,
|
||||
showSeconds: true,
|
||||
format24h: true,
|
||||
theme: "light",
|
||||
};
|
||||
|
||||
// 1초마다 시간 업데이트
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 1000);
|
||||
|
||||
// cleanup: 컴포넌트 unmount 시 타이머 정리
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// 스타일별 렌더링
|
||||
if (config.style === "analog") {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<AnalogClock time={currentTime} theme={config.theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.style === "digital") {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<DigitalClock
|
||||
time={currentTime}
|
||||
timezone={config.timezone}
|
||||
showDate={config.showDate}
|
||||
showSeconds={config.showSeconds}
|
||||
format24h={config.format24h}
|
||||
theme={config.theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 'both' - 아날로그 + 디지털
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{/* 아날로그 시계 (상단 60%) */}
|
||||
<div className="flex-[3]">
|
||||
<AnalogClock time={currentTime} theme={config.theme} />
|
||||
</div>
|
||||
|
||||
{/* 디지털 시계 (하단 40%) */}
|
||||
<div className="flex-[2]">
|
||||
<DigitalClock
|
||||
time={currentTime}
|
||||
timezone={config.timezone}
|
||||
showDate={config.showDate}
|
||||
showSeconds={config.showSeconds}
|
||||
format24h={config.format24h}
|
||||
theme={config.theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
frontend/components/admin/dashboard/widgets/DigitalClock.tsx
Normal file
110
frontend/components/admin/dashboard/widgets/DigitalClock.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
interface DigitalClockProps {
|
||||
time: Date;
|
||||
timezone: string;
|
||||
showDate: boolean;
|
||||
showSeconds: boolean;
|
||||
format24h: boolean;
|
||||
theme: "light" | "dark" | "blue" | "gradient";
|
||||
}
|
||||
|
||||
/**
|
||||
* 디지털 시계 컴포넌트
|
||||
* - 실시간 시간 표시
|
||||
* - 타임존 지원
|
||||
* - 날짜/초 표시 옵션
|
||||
* - 12/24시간 형식 지원
|
||||
*/
|
||||
export function DigitalClock({ time, timezone, showDate, showSeconds, format24h, theme }: 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);
|
||||
|
||||
return (
|
||||
<div className={`flex h-full flex-col items-center justify-center p-4 text-center ${themeClasses.container}`}>
|
||||
{/* 날짜 표시 */}
|
||||
{showDate && dateString && <div className={`mb-3 text-sm font-medium ${themeClasses.date}`}>{dateString}</div>}
|
||||
|
||||
{/* 시간 표시 */}
|
||||
<div className={`text-5xl font-bold tabular-nums ${themeClasses.time}`}>{timeString}</div>
|
||||
|
||||
{/* 타임존 표시 */}
|
||||
<div className={`mt-3 text-xs font-medium ${themeClasses.timezone}`}>{timezoneLabel}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임존 라벨 반환
|
||||
*/
|
||||
function getTimezoneLabel(timezone: string): string {
|
||||
const timezoneLabels: Record<string, string> = {
|
||||
"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) {
|
||||
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",
|
||||
},
|
||||
blue: {
|
||||
container: "bg-gradient-to-br from-blue-400 to-blue-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;
|
||||
}
|
||||
Reference in New Issue
Block a user