ERP-node/frontend/components/ui/custom-calendar.tsx

264 lines
7.0 KiB
TypeScript
Raw Normal View History

2025-10-30 12:03:50 +09:00
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface CustomCalendarProps {
selected?: Date;
onSelect?: (date: Date | undefined) => void;
className?: string;
mode?: "single";
size?: "sm" | "default" | "lg";
}
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export function CustomCalendar({
selected,
onSelect,
className,
mode = "single",
size = "default",
}: CustomCalendarProps) {
// 크기별 클래스 정의
const sizeClasses = {
sm: {
cell: "h-7 w-7 text-xs",
header: "text-xs",
day: "text-[0.7rem]",
nav: "h-6 w-6",
},
default: {
cell: "h-9 w-9 text-sm",
header: "text-[0.8rem]",
day: "text-sm",
nav: "h-7 w-7",
},
lg: {
cell: "h-11 w-11 text-base",
header: "text-sm",
day: "text-base",
nav: "h-8 w-8",
},
};
const currentSize = sizeClasses[size];
const [currentDate, setCurrentDate] = React.useState(selected || new Date());
const [viewYear, setViewYear] = React.useState(currentDate.getFullYear());
const [viewMonth, setViewMonth] = React.useState(currentDate.getMonth());
const getDaysInMonth = (year: number, month: number) => {
return new Date(year, month + 1, 0).getDate();
};
const getFirstDayOfMonth = (year: number, month: number) => {
return new Date(year, month, 1).getDay();
};
const generateCalendarDays = () => {
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
const firstDay = getFirstDayOfMonth(viewYear, viewMonth);
const daysInPrevMonth = getDaysInMonth(viewYear, viewMonth - 1);
const days: Array<{
date: number;
month: "prev" | "current" | "next";
fullDate: Date;
}> = [];
// Previous month days
for (let i = firstDay - 1; i >= 0; i--) {
const date = daysInPrevMonth - i;
days.push({
date,
month: "prev",
fullDate: new Date(viewYear, viewMonth - 1, date),
});
}
// Current month days
for (let i = 1; i <= daysInMonth; i++) {
days.push({
date: i,
month: "current",
fullDate: new Date(viewYear, viewMonth, i),
});
}
// Next month days
const remainingDays = 42 - days.length; // 6 rows * 7 days
for (let i = 1; i <= remainingDays; i++) {
days.push({
date: i,
month: "next",
fullDate: new Date(viewYear, viewMonth + 1, i),
});
}
return days;
};
const handlePrevMonth = () => {
if (viewMonth === 0) {
setViewMonth(11);
setViewYear(viewYear - 1);
} else {
setViewMonth(viewMonth - 1);
}
};
const handleNextMonth = () => {
if (viewMonth === 11) {
setViewMonth(0);
setViewYear(viewYear + 1);
} else {
setViewMonth(viewMonth + 1);
}
};
const handleDateClick = (date: Date) => {
if (onSelect) {
onSelect(date);
}
};
const isToday = (date: Date) => {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
};
const isSelected = (date: Date) => {
if (!selected) return false;
return (
date.getDate() === selected.getDate() &&
date.getMonth() === selected.getMonth() &&
date.getFullYear() === selected.getFullYear()
);
};
const calendarDays = generateCalendarDays();
return (
<div className={cn("p-3", className)}>
{/* Header */}
<div className="flex items-center justify-between gap-2 pb-4">
<Button
variant="outline"
size="icon"
className={cn("bg-transparent p-0 opacity-50 hover:opacity-100", currentSize.nav)}
onClick={handlePrevMonth}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
{/* 월 선택 */}
<Select value={viewMonth.toString()} onValueChange={(value) => setViewMonth(parseInt(value))}>
<SelectTrigger className={cn("w-[110px] font-medium", currentSize.header)}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{MONTHS.map((month, index) => (
<SelectItem key={index} value={index.toString()}>
{month}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 연도 선택 */}
<Select value={viewYear.toString()} onValueChange={(value) => setViewYear(parseInt(value))}>
<SelectTrigger className={cn("w-[80px] font-medium", currentSize.header)}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 100 }, (_, i) => {
const year = new Date().getFullYear() - 50 + i;
return (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<Button
variant="outline"
size="icon"
className={cn("bg-transparent p-0 opacity-50 hover:opacity-100", currentSize.nav)}
onClick={handleNextMonth}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* Days of week */}
<div className="mb-2 grid grid-cols-7 gap-0">
{DAYS.map((day) => (
<div
key={day}
className={cn(
"text-muted-foreground flex items-center justify-center font-normal",
currentSize.cell,
currentSize.day,
)}
>
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7 gap-0">
{calendarDays.map((day, index) => {
const isOutside = day.month !== "current";
const isTodayDate = isToday(day.fullDate);
const isSelectedDate = isSelected(day.fullDate);
return (
<Button
key={index}
variant="ghost"
className={cn(
"p-0 font-normal",
currentSize.cell,
isOutside && "text-muted-foreground opacity-50",
isTodayDate && !isSelectedDate && "bg-accent text-accent-foreground",
isSelectedDate && "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
)}
onClick={() => handleDateClick(day.fullDate)}
>
{day.date}
</Button>
);
})}
</div>
</div>
);
}
CustomCalendar.displayName = "CustomCalendar";