222 lines
6.3 KiB
TypeScript
222 lines
6.3 KiB
TypeScript
"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 (
|
|
<div className="flex h-full flex-col items-center justify-center p-2">
|
|
<svg viewBox="0 0 200 200" className="h-full max-h-[200px] w-full max-w-[200px]">
|
|
{/* 시계판 배경 */}
|
|
<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>
|
|
|
|
{/* 타임존 표시 */}
|
|
{timezoneLabel && (
|
|
<div className="mt-1 text-center text-xs font-medium" style={{ color: colors.number }}>
|
|
{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.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")}`;
|
|
}
|