달력 위젯 구현
This commit is contained in:
parent
4dbb55f6e1
commit
2311729338
|
|
@ -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] 날짜 계산 로직 검증
|
||||
- [ ] 성능 최적화 (필요시)
|
||||
- [ ] 접근성 개선 (필요시)
|
||||
|
||||
## 기술 스택
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
|
||||
<span className="text-sm font-bold text-gray-800">{element.title}</span>
|
||||
<div className="flex gap-1">
|
||||
{/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */}
|
||||
{onConfigure && !(element.type === "widget" && element.subtype === "clock") && (
|
||||
<button
|
||||
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
|
||||
onClick={() => onConfigure(element)}
|
||||
title="설정"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
)}
|
||||
{/* 설정 버튼 (시계, 달력 위젯은 자체 설정 UI 사용) */}
|
||||
{onConfigure &&
|
||||
!(element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar")) && (
|
||||
<button
|
||||
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
|
||||
onClick={() => onConfigure(element)}
|
||||
title="설정"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
)}
|
||||
{/* 삭제 버튼 */}
|
||||
<button
|
||||
className="element-close hover:bg-destructive/100 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
|
||||
|
|
@ -376,6 +385,16 @@ export function CanvasElement({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "calendar" ? (
|
||||
// 달력 위젯 렌더링
|
||||
<div className="h-full w-full">
|
||||
<CalendarWidget
|
||||
element={element}
|
||||
onConfigUpdate={(newConfig) => {
|
||||
onUpdate(element.id, { calendarConfig: newConfig });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// 기타 위젯 렌더링
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -79,8 +79,15 @@ export default function DashboardDesigner() {
|
|||
// 새로운 요소 생성 (고정 그리드 기반 기본 크기)
|
||||
const createElement = useCallback(
|
||||
(type: ElementType, subtype: ElementSubtype, x: number, y: number) => {
|
||||
// 기본 크기: 차트는 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() {
|
|||
<div className="relative flex-1 overflow-auto border-r-2 border-gray-300 bg-gray-100">
|
||||
{/* 편집 중인 대시보드 표시 */}
|
||||
{dashboardTitle && (
|
||||
<div className="bg-accent0 absolute left-6 top-6 z-10 rounded-lg px-3 py-1 text-sm font-medium text-white shadow-lg">
|
||||
<div className="bg-accent0 absolute top-6 left-6 z-10 rounded-lg px-3 py-1 text-sm font-medium text-white shadow-lg">
|
||||
📝 편집 중: {dashboardTitle}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -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 "위젯 내용이 여기에 표시됩니다";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,14 @@ export function DashboardSidebar() {
|
|||
onDragStart={handleDragStart}
|
||||
className="border-l-4 border-teal-500"
|
||||
/>
|
||||
<DraggableItem
|
||||
icon="📅"
|
||||
title="달력 위젯"
|
||||
type="widget"
|
||||
subtype="calendar"
|
||||
onDragStart={handleDragStart}
|
||||
className="border-l-4 border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ClockConfigModal
|
||||
config={
|
||||
element.clockConfig || {
|
||||
style: "digital",
|
||||
timezone: "Asia/Seoul",
|
||||
showDate: true,
|
||||
showSeconds: true,
|
||||
format24h: true,
|
||||
theme: "light",
|
||||
}
|
||||
}
|
||||
onSave={handleClockConfigSave}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
||||
<div className="flex h-[80vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl">
|
||||
|
|
|
|||
|
|
@ -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; // 주차 표시 (선택)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CalendarConfig>(config);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(localConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex max-h-[600px] flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b p-4">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<span>📅</span>
|
||||
달력 설정
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* 내용 - 스크롤 가능 */}
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{/* 뷰 타입 선택 (현재는 month만) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">뷰 타입</Label>
|
||||
<Select
|
||||
value={localConfig.view}
|
||||
onValueChange={(value) => setLocalConfig({ ...localConfig, view: value as any })}
|
||||
>
|
||||
<SelectTrigger className="w-full" size="sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="month">월간 뷰</SelectItem>
|
||||
{/* <SelectItem value="week">주간 뷰 (준비 중)</SelectItem>
|
||||
<SelectItem value="day">일간 뷰 (준비 중)</SelectItem> */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 시작 요일 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">주 시작 요일</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={localConfig.startWeekOn === "sunday" ? "default" : "outline"}
|
||||
onClick={() => setLocalConfig({ ...localConfig, startWeekOn: "sunday" })}
|
||||
size="sm"
|
||||
>
|
||||
일요일
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={localConfig.startWeekOn === "monday" ? "default" : "outline"}
|
||||
onClick={() => setLocalConfig({ ...localConfig, startWeekOn: "monday" })}
|
||||
size="sm"
|
||||
>
|
||||
월요일
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 테마 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">테마</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Button
|
||||
key={theme.value}
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => 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"
|
||||
>
|
||||
<div className={`${theme.gradient} ${theme.text} w-full rounded px-3 py-2 text-xs font-medium`}>
|
||||
{theme.label}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 사용자 지정 색상 */}
|
||||
{localConfig.theme === "custom" && (
|
||||
<Card className="mt-2 border p-3">
|
||||
<Label className="mb-2 block text-xs font-medium">강조 색상</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={localConfig.customColor || "#3b82f6"}
|
||||
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||
className="h-10 w-16 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={localConfig.customColor || "#3b82f6"}
|
||||
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||
placeholder="#3b82f6"
|
||||
className="flex-1 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 표시 옵션 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">표시 옵션</Label>
|
||||
<div className="space-y-2">
|
||||
{/* 오늘 강조 */}
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">📍</span>
|
||||
<Label className="cursor-pointer text-sm">오늘 날짜 강조</Label>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localConfig.highlightToday}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, highlightToday: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 주말 강조 */}
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🎨</span>
|
||||
<Label className="cursor-pointer text-sm">주말 색상 강조</Label>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localConfig.highlightWeekends}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, highlightWeekends: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 공휴일 표시 */}
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🎉</span>
|
||||
<Label className="cursor-pointer text-sm">공휴일 표시</Label>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localConfig.showHolidays}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, showHolidays: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex justify-end gap-2 border-t p-4">
|
||||
<Button variant="outline" size="sm" onClick={onClose}>
|
||||
취소
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/* 헤더 - 네비게이션 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-2">
|
||||
{/* 이전 월 버튼 */}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handlePrevMonth}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* 현재 년월 표시 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold">
|
||||
{currentYear}년 {getMonthName(currentMonth)}
|
||||
</span>
|
||||
{!isCompact && (
|
||||
<Button variant="outline" size="sm" className="h-6 px-2 text-xs" onClick={handleToday}>
|
||||
오늘
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 다음 월 버튼 */}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleNextMonth}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 달력 콘텐츠 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{config.view === "month" && <MonthView days={calendarDays} config={config} isCompact={isCompact} />}
|
||||
{/* 추후 WeekView, DayView 추가 가능 */}
|
||||
</div>
|
||||
|
||||
{/* 설정 버튼 - 우측 하단 */}
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[450px] p-0" align="end">
|
||||
<CalendarSettings config={config} onSave={handleSaveSettings} onClose={() => setSettingsOpen(false)} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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<ClockConfig>(config);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(localConfig);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden p-0">
|
||||
<DialogHeader className="border-b p-6">
|
||||
<DialogTitle className="flex items-center gap-2 text-xl">
|
||||
<span>⏰</span>
|
||||
시계 위젯 설정
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 내용 - 스크롤 가능 */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-4">
|
||||
{/* 스타일 선택 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">시계 스타일</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ value: "digital", label: "디지털", icon: "🔢" },
|
||||
{ value: "analog", label: "아날로그", icon: "🕐" },
|
||||
{ value: "both", label: "둘 다", icon: "⏰" },
|
||||
].map((style) => (
|
||||
<Button
|
||||
key={style.value}
|
||||
type="button"
|
||||
variant={localConfig.style === style.value ? "default" : "outline"}
|
||||
onClick={() => setLocalConfig({ ...localConfig, style: style.value as any })}
|
||||
className="flex h-auto flex-col items-center gap-2 p-4"
|
||||
>
|
||||
<span className="text-3xl">{style.icon}</span>
|
||||
<span className="text-sm font-medium">{style.label}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 타임존 선택 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">타임존</Label>
|
||||
<Select
|
||||
value={localConfig.timezone}
|
||||
onValueChange={(value) => setLocalConfig({ ...localConfig, timezone: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Asia/Seoul">🇰🇷 서울 (KST)</SelectItem>
|
||||
<SelectItem value="Asia/Tokyo">🇯🇵 도쿄 (JST)</SelectItem>
|
||||
<SelectItem value="Asia/Shanghai">🇨🇳 베이징 (CST)</SelectItem>
|
||||
<SelectItem value="America/New_York">🇺🇸 뉴욕 (EST)</SelectItem>
|
||||
<SelectItem value="America/Los_Angeles">🇺🇸 LA (PST)</SelectItem>
|
||||
<SelectItem value="Europe/London">🇬🇧 런던 (GMT)</SelectItem>
|
||||
<SelectItem value="Europe/Paris">🇫🇷 파리 (CET)</SelectItem>
|
||||
<SelectItem value="Australia/Sydney">🇦🇺 시드니 (AEDT)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 테마 선택 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">테마</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Button
|
||||
key={theme.value}
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => 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" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`${theme.gradient} ${theme.text} w-full rounded p-3 text-center text-xs font-medium`}>
|
||||
{theme.label}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 사용자 지정 색상 선택 */}
|
||||
{localConfig.theme === "custom" && (
|
||||
<Card className="border p-4">
|
||||
<Label className="mb-2 block text-sm font-medium">배경 색상 선택</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={localConfig.customColor || "#3b82f6"}
|
||||
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||
className="h-12 w-20 cursor-pointer"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={localConfig.customColor || "#3b82f6"}
|
||||
onChange={(e) => setLocalConfig({ ...localConfig, customColor: e.target.value })}
|
||||
placeholder="#3b82f6"
|
||||
className="font-mono"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-xs">시계의 배경색이나 강조색으로 사용됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 옵션 토글 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">표시 옵션</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* 날짜 표시 */}
|
||||
<Card className="hover:bg-accent flex cursor-pointer flex-col items-center justify-center border p-4 text-center transition-colors">
|
||||
<span className="mb-2 text-2xl">📅</span>
|
||||
<Label className="mb-1 cursor-pointer text-sm font-medium">날짜 표시</Label>
|
||||
<Switch
|
||||
checked={localConfig.showDate}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, showDate: checked })}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 초 표시 */}
|
||||
<Card className="hover:bg-accent flex cursor-pointer flex-col items-center justify-center border p-4 text-center transition-colors">
|
||||
<span className="mb-2 text-2xl">⏱️</span>
|
||||
<Label className="mb-1 cursor-pointer text-sm font-medium">초 표시</Label>
|
||||
<Switch
|
||||
checked={localConfig.showSeconds}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, showSeconds: checked })}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 24시간 형식 */}
|
||||
<Card className="hover:bg-accent flex cursor-pointer flex-col items-center justify-center border p-4 text-center transition-colors">
|
||||
<span className="mb-2 text-2xl">🕐</span>
|
||||
<Label className="mb-1 cursor-pointer text-sm font-medium">24시간 형식</Label>
|
||||
<Switch
|
||||
checked={localConfig.format24h}
|
||||
onCheckedChange={(checked) => setLocalConfig({ ...localConfig, format24h: checked })}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t p-6">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="flex h-full flex-col p-2">
|
||||
{/* 요일 헤더 */}
|
||||
{!isCompact && (
|
||||
<div className="mb-2 grid grid-cols-7 gap-1">
|
||||
{weekDayNames.map((name, index) => {
|
||||
const isWeekend = config.startWeekOn === "sunday" ? index === 0 || index === 6 : index === 5 || index === 6;
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`text-center text-xs font-semibold ${isWeekend && config.highlightWeekends ? "text-red-600" : "text-gray-600"}`}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 날짜 그리드 */}
|
||||
<div className="grid flex-1 grid-cols-7 gap-1">
|
||||
{days.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={getDayCellClass(day)}
|
||||
style={{
|
||||
backgroundColor:
|
||||
config.highlightToday && day.isToday ? themeStyles.todayBg : undefined,
|
||||
color:
|
||||
config.showHolidays && day.isHoliday && day.isCurrentMonth
|
||||
? themeStyles.holidayText
|
||||
: undefined,
|
||||
}}
|
||||
title={day.isHoliday ? day.holidayName : undefined}
|
||||
>
|
||||
{day.day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue