diff --git a/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md index 84f2a4dc..e127be43 100644 --- a/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md +++ b/frontend/components/admin/dashboard/CALENDAR_WIDGET_PLAN.md @@ -34,57 +34,57 @@ ### ✅ Step 1: 타입 정의 -- [ ] `CalendarConfig` 인터페이스 정의 -- [ ] `types.ts`에 달력 설정 타입 추가 -- [ ] 요소 타입에 'calendar' subtype 추가 +- [x] `CalendarConfig` 인터페이스 정의 +- [x] `types.ts`에 달력 설정 타입 추가 +- [x] 요소 타입에 'calendar' subtype 추가 ### ✅ Step 2: 기본 달력 컴포넌트 -- [ ] `CalendarWidget.tsx` - 메인 위젯 컴포넌트 -- [ ] `MonthView.tsx` - 월간 달력 뷰 -- [ ] `WeekView.tsx` - 주간 달력 뷰 (선택) -- [ ] 날짜 계산 유틸리티 함수 +- [x] `CalendarWidget.tsx` - 메인 위젯 컴포넌트 +- [x] `MonthView.tsx` - 월간 달력 뷰 +- [x] 날짜 계산 유틸리티 함수 (`calendarUtils.ts`) +- [ ] `WeekView.tsx` - 주간 달력 뷰 (향후 추가) ### ✅ Step 3: 달력 네비게이션 -- [ ] 이전/다음 월 이동 버튼 -- [ ] 오늘로 돌아가기 버튼 -- [ ] 월/연도 선택 드롭다운 +- [x] 이전/다음 월 이동 버튼 +- [x] 오늘로 돌아가기 버튼 +- [ ] 월/연도 선택 드롭다운 (향후 추가) ### ✅ Step 4: 설정 UI -- [ ] `CalendarSettings.tsx` - Popover 내장 설정 컴포넌트 -- [ ] 뷰 타입 선택 (월간/주간/일간) -- [ ] 시작 요일 설정 -- [ ] 테마 선택 -- [ ] 표시 옵션 (주말 강조, 공휴일 등) +- [x] `CalendarSettings.tsx` - Popover 내장 설정 컴포넌트 +- [x] 뷰 타입 선택 (월간 - 현재 구현) +- [x] 시작 요일 설정 +- [x] 테마 선택 (light/dark/custom) +- [x] 표시 옵션 (주말 강조, 공휴일, 오늘 강조) ### ✅ Step 5: 스타일링 -- [ ] 달력 그리드 레이아웃 -- [ ] 날짜 셀 디자인 -- [ ] 오늘 날짜 하이라이트 -- [ ] 주말/평일 구분 -- [ ] 반응형 디자인 (크기별 최적화) +- [x] 달력 그리드 레이아웃 +- [x] 날짜 셀 디자인 +- [x] 오늘 날짜 하이라이트 +- [x] 주말/평일 구분 +- [x] 반응형 디자인 (크기별 최적화) ### ✅ Step 6: 통합 -- [ ] `DashboardSidebar`에 달력 위젯 추가 -- [ ] `CanvasElement`에서 달력 위젯 렌더링 -- [ ] `DashboardDesigner`에 기본값 설정 +- [x] `DashboardSidebar`에 달력 위젯 추가 +- [x] `CanvasElement`에서 달력 위젯 렌더링 +- [x] `DashboardDesigner`에 기본값 설정 ### ✅ Step 7: 공휴일 데이터 -- [ ] 한국 공휴일 데이터 정의 -- [ ] 공휴일 표시 기능 -- [ ] 공휴일 이름 툴팁 +- [x] 한국 공휴일 데이터 정의 +- [x] 공휴일 표시 기능 +- [x] 공휴일 이름 툴팁 ### ✅ Step 8: 테스트 및 최적화 -- [ ] 다양한 크기에서 테스트 -- [ ] 날짜 계산 로직 검증 -- [ ] 성능 최적화 -- [ ] 접근성 개선 +- [ ] 다양한 크기에서 테스트 (사용자 테스트 필요) +- [x] 날짜 계산 로직 검증 +- [ ] 성능 최적화 (필요시) +- [ ] 접근성 개선 (필요시) ## 기술 스택 diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index d830263d..aced2eb9 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -19,6 +19,8 @@ const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/Exch // 시계 위젯 임포트 import { ClockWidget } from "./widgets/ClockWidget"; +// 달력 위젯 임포트 +import { CalendarWidget } from "./widgets/CalendarWidget"; interface CanvasElementProps { element: DashboardElement; @@ -130,26 +132,30 @@ export function CanvasElement({ let newX = resizeStart.elementX; let newY = resizeStart.elementY; - const minSize = GRID_CONFIG.CELL_SIZE * 2; // 최소 2셀 + // 최소 크기 설정: 달력은 2x3, 나머지는 2x2 + const minWidthCells = 2; + const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2; + const minWidth = GRID_CONFIG.CELL_SIZE * minWidthCells; + const minHeight = GRID_CONFIG.CELL_SIZE * minHeightCells; switch (resizeStart.handle) { case "se": // 오른쪽 아래 - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); break; case "sw": // 왼쪽 아래 - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width - deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); newX = resizeStart.elementX + deltaX; break; case "ne": // 오른쪽 위 - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height - deltaY); newY = resizeStart.elementY + deltaY; break; case "nw": // 왼쪽 위 - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + newWidth = Math.max(minWidth, resizeStart.width - deltaX); + newHeight = Math.max(minHeight, resizeStart.height - deltaY); newX = resizeStart.elementX + deltaX; newY = resizeStart.elementY + deltaY; break; @@ -281,6 +287,8 @@ export function CanvasElement({ return "bg-gradient-to-br from-cyan-400 to-indigo-800"; case "clock": return "bg-gradient-to-br from-teal-400 to-cyan-600"; + case "calendar": + return "bg-gradient-to-br from-indigo-400 to-purple-600"; default: return "bg-gray-200"; } @@ -310,16 +318,17 @@ export function CanvasElement({
{element.title}
- {/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */} - {onConfigure && !(element.type === "widget" && element.subtype === "clock") && ( - - )} + {/* 설정 버튼 (시계, 달력 위젯은 자체 설정 UI 사용) */} + {onConfigure && + !(element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) && ( + + )} {/* 삭제 버튼 */}
+ ) : element.type === "widget" && element.subtype === "calendar" ? ( + // 달력 위젯 렌더링 +
+ { + onUpdate(element.id, { calendarConfig: newConfig }); + }} + /> +
) : ( // 기타 위젯 렌더링
{ - // 기본 크기: 차트는 4x3 셀, 위젯은 2x2 셀 - const defaultCells = type === "chart" ? { width: 4, height: 3 } : { width: 2, height: 2 }; + // 기본 크기 설정 + let defaultCells = { width: 2, height: 2 }; // 기본 위젯 크기 + + if (type === "chart") { + defaultCells = { width: 4, height: 3 }; // 차트 + } else if (type === "widget" && subtype === "calendar") { + defaultCells = { width: 2, height: 3 }; // 달력 최소 크기 + } + const cellWithGap = GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP; const defaultWidth = defaultCells.width * cellWithGap - GRID_CONFIG.GAP; @@ -232,7 +239,7 @@ export default function DashboardDesigner() {
{/* 편집 중인 대시보드 표시 */} {dashboardTitle && ( -
+
📝 편집 중: {dashboardTitle}
)} @@ -291,6 +298,8 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string { return "☁️ 날씨 위젯"; case "clock": return "⏰ 시계 위젯"; + case "calendar": + return "📅 달력 위젯"; default: return "🔧 위젯"; } @@ -319,6 +328,8 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string { return "서울\n23°C\n구름 많음"; case "clock": return "clock"; + case "calendar": + return "calendar"; default: return "위젯 내용이 여기에 표시됩니다"; } diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx index 41888172..82ce27c3 100644 --- a/frontend/components/admin/dashboard/DashboardSidebar.tsx +++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx @@ -111,6 +111,14 @@ export function DashboardSidebar() { onDragStart={handleDragStart} className="border-l-4 border-teal-500" /> +
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index dc9d3f32..31fdee8b 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -1,10 +1,9 @@ "use client"; import React, { useState, useCallback } from "react"; -import { DashboardElement, ChartDataSource, ChartConfig, QueryResult, ClockConfig } from "./types"; +import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types"; import { QueryEditor } from "./QueryEditor"; import { ChartConfigPanel } from "./ChartConfigPanel"; -import { ClockConfigModal } from "./widgets/ClockConfigModal"; interface ElementConfigModalProps { element: DashboardElement; @@ -57,46 +56,14 @@ 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") { + // 시계, 달력 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음 + if (element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) { return null; } - // 이전 코드 호환성 유지 (아래 주석 처리된 코드는 제거 예정) - if (false && element.type === "widget" && element.subtype === "clock") { - return ( - - ); - } - return (
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index d304c9f3..2ac0bb6d 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -14,7 +14,8 @@ export type ElementSubtype = | "combo" // 차트 타입 | "exchange" | "weather" - | "clock"; // 위젯 타입 + | "clock" + | "calendar"; // 위젯 타입 export interface Position { x: number; @@ -37,6 +38,7 @@ export interface DashboardElement { dataSource?: ChartDataSource; // 데이터 소스 설정 chartConfig?: ChartConfig; // 차트 설정 clockConfig?: ClockConfig; // 시계 설정 + calendarConfig?: CalendarConfig; // 달력 설정 } export interface DragData { @@ -86,3 +88,15 @@ export interface ClockConfig { theme: "light" | "dark" | "custom"; // 테마 customColor?: string; // 사용자 지정 색상 (custom 테마일 때) } + +// 달력 위젯 설정 +export interface CalendarConfig { + view: "month" | "week" | "day"; // 뷰 타입 + startWeekOn: "monday" | "sunday"; // 주 시작 요일 + highlightWeekends: boolean; // 주말 강조 + highlightToday: boolean; // 오늘 강조 + showHolidays: boolean; // 공휴일 표시 + theme: "light" | "dark" | "custom"; // 테마 + customColor?: string; // 사용자 지정 색상 + showWeekNumbers?: boolean; // 주차 표시 (선택) +} diff --git a/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx b/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx new file mode 100644 index 00000000..89633cc8 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/CalendarSettings.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useState } from "react"; +import { CalendarConfig } 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 CalendarSettingsProps { + config: CalendarConfig; + onSave: (config: CalendarConfig) => void; + onClose: () => void; +} + +/** + * 달력 위젯 설정 UI (Popover 내부용) + */ +export function CalendarSettings({ config, onSave, onClose }: CalendarSettingsProps) { + const [localConfig, setLocalConfig] = useState(config); + + const handleSave = () => { + onSave(localConfig); + }; + + return ( +
+ {/* 헤더 */} +
+

+ 📅 + 달력 설정 +

+
+ + {/* 내용 - 스크롤 가능 */} +
+ {/* 뷰 타입 선택 (현재는 month만) */} +
+ + +
+ + + + {/* 시작 요일 선택 */} +
+ +
+ + +
+
+ + + + {/* 테마 선택 */} +
+ +
+ {[ + { + 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-10 w-16 cursor-pointer" + /> + setLocalConfig({ ...localConfig, customColor: e.target.value })} + placeholder="#3b82f6" + className="flex-1 font-mono text-xs" + /> +
+
+ )} +
+ + + + {/* 표시 옵션 */} +
+ +
+ {/* 오늘 강조 */} +
+
+ 📍 + +
+ setLocalConfig({ ...localConfig, highlightToday: checked })} + /> +
+ + {/* 주말 강조 */} +
+
+ 🎨 + +
+ setLocalConfig({ ...localConfig, highlightWeekends: checked })} + /> +
+ + {/* 공휴일 표시 */} +
+
+ 🎉 + +
+ setLocalConfig({ ...localConfig, showHolidays: checked })} + /> +
+
+
+
+ + {/* 푸터 */} +
+ + +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx b/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx new file mode 100644 index 00000000..4f54ac65 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/CalendarWidget.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { DashboardElement, CalendarConfig } from "../types"; +import { MonthView } from "./MonthView"; +import { CalendarSettings } from "./CalendarSettings"; +import { generateCalendarDays, getMonthName, navigateMonth } from "./calendarUtils"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Settings, ChevronLeft, ChevronRight, Calendar } from "lucide-react"; + +interface CalendarWidgetProps { + element: DashboardElement; + onConfigUpdate?: (config: CalendarConfig) => void; +} + +/** + * 달력 위젯 메인 컴포넌트 + * - 월간/주간/일간 뷰 지원 + * - 네비게이션 (이전/다음 월, 오늘) + * - 내장 설정 UI + */ +export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps) { + // 현재 표시 중인 년/월 + const today = new Date(); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [currentMonth, setCurrentMonth] = useState(today.getMonth()); + const [settingsOpen, setSettingsOpen] = useState(false); + + // 기본 설정값 + const config = element.calendarConfig || { + view: "month", + startWeekOn: "sunday", + highlightWeekends: true, + highlightToday: true, + showHolidays: true, + theme: "light", + }; + + // 설정 저장 핸들러 + const handleSaveSettings = (newConfig: CalendarConfig) => { + onConfigUpdate?.(newConfig); + setSettingsOpen(false); + }; + + // 이전 월로 이동 + const handlePrevMonth = () => { + const { year, month } = navigateMonth(currentYear, currentMonth, "prev"); + setCurrentYear(year); + setCurrentMonth(month); + }; + + // 다음 월로 이동 + const handleNextMonth = () => { + const { year, month } = navigateMonth(currentYear, currentMonth, "next"); + setCurrentYear(year); + setCurrentMonth(month); + }; + + // 오늘로 돌아가기 + const handleToday = () => { + setCurrentYear(today.getFullYear()); + setCurrentMonth(today.getMonth()); + }; + + // 달력 날짜 생성 + const calendarDays = generateCalendarDays(currentYear, currentMonth, config.startWeekOn); + + // 크기에 따른 컴팩트 모드 판단 + const isCompact = element.size.width < 400 || element.size.height < 400; + + return ( +
+ {/* 헤더 - 네비게이션 */} +
+ {/* 이전 월 버튼 */} + + + {/* 현재 년월 표시 */} +
+ + {currentYear}년 {getMonthName(currentMonth)} + + {!isCompact && ( + + )} +
+ + {/* 다음 월 버튼 */} + +
+ + {/* 달력 콘텐츠 */} +
+ {config.view === "month" && } + {/* 추후 WeekView, DayView 추가 가능 */} +
+ + {/* 설정 버튼 - 우측 하단 */} +
+ + + + + + setSettingsOpen(false)} /> + + +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx b/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx deleted file mode 100644 index 26067b48..00000000 --- a/frontend/components/admin/dashboard/widgets/ClockConfigModal.tsx +++ /dev/null @@ -1,205 +0,0 @@ -"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/MonthView.tsx b/frontend/components/admin/dashboard/widgets/MonthView.tsx new file mode 100644 index 00000000..c0fd3871 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/MonthView.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { CalendarConfig } from "../types"; +import { CalendarDay, getWeekDayNames } from "./calendarUtils"; + +interface MonthViewProps { + days: CalendarDay[]; + config: CalendarConfig; + isCompact?: boolean; // 작은 크기 (2x2, 3x3) +} + +/** + * 월간 달력 뷰 컴포넌트 + */ +export function MonthView({ days, config, isCompact = false }: MonthViewProps) { + const weekDayNames = getWeekDayNames(config.startWeekOn); + + // 테마별 스타일 + const getThemeStyles = () => { + if (config.theme === "custom" && config.customColor) { + return { + todayBg: config.customColor, + holidayText: config.customColor, + weekendText: "#dc2626", + }; + } + + if (config.theme === "dark") { + return { + todayBg: "#3b82f6", + holidayText: "#f87171", + weekendText: "#f87171", + }; + } + + // light 테마 + return { + todayBg: "#3b82f6", + holidayText: "#dc2626", + weekendText: "#dc2626", + }; + }; + + const themeStyles = getThemeStyles(); + + // 날짜 셀 스타일 클래스 + const getDayCellClass = (day: CalendarDay) => { + const baseClass = "flex aspect-square items-center justify-center rounded-lg transition-colors"; + const sizeClass = isCompact ? "text-xs" : "text-sm"; + + let colorClass = "text-gray-700"; + + // 현재 월이 아닌 날짜 + if (!day.isCurrentMonth) { + colorClass = "text-gray-300"; + } + // 오늘 + else if (config.highlightToday && day.isToday) { + colorClass = "text-white font-bold"; + } + // 공휴일 + else if (config.showHolidays && day.isHoliday) { + colorClass = "font-semibold"; + } + // 주말 + else if (config.highlightWeekends && day.isWeekend) { + colorClass = "text-red-600"; + } + + const bgClass = config.highlightToday && day.isToday ? "" : "hover:bg-gray-100"; + + return `${baseClass} ${sizeClass} ${colorClass} ${bgClass}`; + }; + + return ( +
+ {/* 요일 헤더 */} + {!isCompact && ( +
+ {weekDayNames.map((name, index) => { + const isWeekend = config.startWeekOn === "sunday" ? index === 0 || index === 6 : index === 5 || index === 6; + return ( +
+ {name} +
+ ); + })} +
+ )} + + {/* 날짜 그리드 */} +
+ {days.map((day, index) => ( +
+ {day.day} +
+ ))} +
+
+ ); +} + diff --git a/frontend/components/admin/dashboard/widgets/calendarUtils.ts b/frontend/components/admin/dashboard/widgets/calendarUtils.ts new file mode 100644 index 00000000..4bdb8deb --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/calendarUtils.ts @@ -0,0 +1,162 @@ +/** + * 달력 유틸리티 함수 + */ + +// 한국 공휴일 데이터 (2025년 기준) +export interface Holiday { + date: string; // 'MM-DD' 형식 + name: string; + isRecurring: boolean; +} + +export const KOREAN_HOLIDAYS: Holiday[] = [ + { date: "01-01", name: "신정", isRecurring: true }, + { date: "01-28", name: "설날 연휴", isRecurring: false }, + { date: "01-29", name: "설날", isRecurring: false }, + { date: "01-30", name: "설날 연휴", isRecurring: false }, + { date: "03-01", name: "삼일절", isRecurring: true }, + { date: "05-05", name: "어린이날", isRecurring: true }, + { date: "06-06", name: "현충일", isRecurring: true }, + { date: "08-15", name: "광복절", isRecurring: true }, + { date: "10-03", name: "개천절", isRecurring: true }, + { date: "10-09", name: "한글날", isRecurring: true }, + { date: "12-25", name: "크리스마스", isRecurring: true }, +]; + +/** + * 특정 월의 첫 날 Date 객체 반환 + */ +export function getFirstDayOfMonth(year: number, month: number): Date { + return new Date(year, month, 1); +} + +/** + * 특정 월의 마지막 날짜 반환 + */ +export function getLastDateOfMonth(year: number, month: number): number { + return new Date(year, month + 1, 0).getDate(); +} + +/** + * 특정 월의 첫 날의 요일 반환 (0=일요일, 1=월요일, ...) + */ +export function getFirstDayOfWeek(year: number, month: number): number { + return new Date(year, month, 1).getDay(); +} + +/** + * 달력 그리드에 표시할 날짜 배열 생성 + * @param year 년도 + * @param month 월 (0-11) + * @param startWeekOn 주 시작 요일 ('monday' | 'sunday') + * @returns 6주 * 7일 = 42개의 날짜 정보 배열 + */ +export interface CalendarDay { + date: Date; + day: number; + isCurrentMonth: boolean; + isToday: boolean; + isWeekend: boolean; + isHoliday: boolean; + holidayName?: string; +} + +export function generateCalendarDays( + year: number, + month: number, + startWeekOn: "monday" | "sunday" = "sunday", +): CalendarDay[] { + const days: CalendarDay[] = []; + const firstDay = getFirstDayOfWeek(year, month); + const lastDate = getLastDateOfMonth(year, month); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // 시작 오프셋 계산 + let startOffset = firstDay; + if (startWeekOn === "monday") { + startOffset = firstDay === 0 ? 6 : firstDay - 1; + } + + // 이전 달 날짜들 + const prevMonthLastDate = getLastDateOfMonth(year, month - 1); + for (let i = startOffset - 1; i >= 0; i--) { + const date = new Date(year, month - 1, prevMonthLastDate - i); + days.push(createCalendarDay(date, false, today)); + } + + // 현재 달 날짜들 + for (let day = 1; day <= lastDate; day++) { + const date = new Date(year, month, day); + days.push(createCalendarDay(date, true, today)); + } + + // 다음 달 날짜들 (42개 채우기) + const remainingDays = 42 - days.length; + for (let day = 1; day <= remainingDays; day++) { + const date = new Date(year, month + 1, day); + days.push(createCalendarDay(date, false, today)); + } + + return days; +} + +/** + * CalendarDay 객체 생성 + */ +function createCalendarDay(date: Date, isCurrentMonth: boolean, today: Date): CalendarDay { + const dayOfWeek = date.getDay(); + const isToday = date.getTime() === today.getTime(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + // 공휴일 체크 + const monthStr = String(date.getMonth() + 1).padStart(2, "0"); + const dayStr = String(date.getDate()).padStart(2, "0"); + const dateKey = `${monthStr}-${dayStr}`; + const holiday = KOREAN_HOLIDAYS.find((h) => h.date === dateKey); + + return { + date, + day: date.getDate(), + isCurrentMonth, + isToday, + isWeekend, + isHoliday: !!holiday, + holidayName: holiday?.name, + }; +} + +/** + * 요일 이름 배열 반환 + */ +export function getWeekDayNames(startWeekOn: "monday" | "sunday" = "sunday"): string[] { + const sundayFirst = ["일", "월", "화", "수", "목", "금", "토"]; + const mondayFirst = ["월", "화", "수", "목", "금", "토", "일"]; + return startWeekOn === "monday" ? mondayFirst : sundayFirst; +} + +/** + * 월 이름 반환 + */ +export function getMonthName(month: number): string { + const months = ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"]; + return months[month]; +} + +/** + * 이전/다음 월로 이동 + */ +export function navigateMonth(year: number, month: number, direction: "prev" | "next"): { year: number; month: number } { + if (direction === "prev") { + if (month === 0) { + return { year: year - 1, month: 11 }; + } + return { year, month: month - 1 }; + } else { + if (month === 11) { + return { year: year + 1, month: 0 }; + } + return { year, month: month + 1 }; + } +} +