/** @jsxImportSource @emotion/react */
import { colors } from '@/libs/themes';
import { Interpolation, Theme } from '@emotion/react';
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
import CalendarTitle from './CalendarTitle';
const daysOfWeek = ['일', '월', '화', '수', '목', '금', '토'];
interface CalendarProps {
minDate?: Date | number | unknown;
maxDate?: Date | number | unknown;
date: Date | null;
onClick?: (date: Date) => void;
format?: 'yyyy-mm-dd' | 'yyyy-mm' | 'yyyy';
}
const Calendar = ({ minDate, maxDate, date, onClick, format = 'yyyy-mm-dd' }: CalendarProps) => {
const [selectedDate, setSelectedDate] = useState(() => date || new Date());
useEffect(() => {
if (date instanceof Date || date === null) setSelectedDate(date || new Date());
}, [date]);
const [currentMonth, setCurrentMonth] = useState(selectedDate?.getMonth());
const [currentYear, setCurrentYear] = useState(selectedDate?.getFullYear());
const [isFormat, setIsFormat] = useState(format);
const today = new Date();
const todayDate = today.getDate();
const todayMonth = today.getMonth();
const todayYear = today.getFullYear();
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
const calendarDays = [];
for (let i = 1; i <= daysInMonth + firstDayOfMonth; i++) {
calendarDays.push(i > firstDayOfMonth ? i - firstDayOfMonth : null);
}
const prevMonth = () => {
setCurrentMonth(prev => (prev === 0 ? 11 : prev - 1));
if (currentMonth === 0) setCurrentYear(prev => prev - 1);
};
const nextMonth = () => {
setCurrentMonth(prev => (prev === 11 ? 0 : prev + 1));
if (currentMonth === 11) setCurrentYear(prev => prev + 1);
};
const prevYear = () => setCurrentYear(currentYear - 1);
const nextYear = () => setCurrentYear(currentYear + 1);
const isSelectedDate = (day: number | null) => {
return (
selectedDate &&
day === selectedDate.getDate() &&
currentMonth === selectedDate.getMonth() &&
currentYear === selectedDate.getFullYear()
);
};
const [yearRange, setYearRange] = useState({
startYear: currentYear - (currentYear % 10),
endYear: currentYear - (currentYear % 10) + 9,
});
const nextDecade = () =>
setYearRange(prev => ({
startYear: prev.startYear + 10,
endYear: prev.endYear + 10,
}));
const prevDecade = () =>
setYearRange(prev => ({
startYear: prev.startYear - 10,
endYear: prev.endYear - 10,
}));
//
// yyyy-mm-dd > 클릭핸들러
const selectDay = (day: any) => {
const newDate = new Date(currentYear, currentMonth, day);
if ((minDate && newDate < minDate) || (maxDate && newDate > maxDate)) return;
onClick && onClick(newDate);
};
//
// yyyy > 클릭 핸들러
const selectYear = (year: number) => {
const newDate = new Date(year, 0, 1);
// yyyy 포맷일 때만 minDate/maxDate 체크
if (format === 'yyyy' && ((minDate && newDate < minDate) || (maxDate && newDate > maxDate))) return;
setCurrentYear(year);
format !== 'yyyy' && setIsFormat('yyyy-mm');
format === 'yyyy' && onClick && onClick(newDate);
};
return (
{isFormat === 'yyyy' && (
<>
{Array.from({ length: 9 }, (_, i) => yearRange.startYear + i).map(year => {
const date = new Date(year, 0, 1);
// yyyy 포맷일 때만 selectable 체크, 그 외에는 항상 true
const selectable =
format === 'yyyy' ? (!minDate || date >= minDate) && (!maxDate || date <= maxDate) : true;
const isToday = todayYear === year;
const isSelected = selectedDate.getFullYear() === year;
return (
selectYear(year)}>
);
})}
>
)}
{isFormat === 'yyyy-mm' && (
<>
setIsFormat('yyyy')}
title={`${currentYear}년`}
/>
{Array.from({ length: 12 }, (_, i) => i).map(month => {
const date = new Date(currentYear, month, 1);
const lastDayOfMonth = new Date(currentYear, month + 1, 0);
// Check if any day in month is selectable for yyyy-mm-dd format
let monthSelectable = false;
if (format === 'yyyy-mm-dd') {
for (let day = 1; day <= lastDayOfMonth.getDate(); day++) {
const dayDate = new Date(currentYear, month, day);
if ((!minDate || dayDate >= minDate) && (!maxDate || dayDate <= maxDate)) {
monthSelectable = true;
break;
}
}
} else {
monthSelectable = (!minDate || date >= minDate) && (!maxDate || date <= maxDate);
}
const isToday = todayYear === currentYear && todayMonth === month;
const isSelectedMonth = selectedDate.getMonth() === month && selectedDate.getFullYear() === currentYear;
return (
{
if (!monthSelectable) return;
const newDate = new Date(currentYear, month, 1);
setCurrentMonth(month);
setCurrentYear(currentYear);
format === 'yyyy-mm-dd' && setIsFormat('yyyy-mm-dd');
format === 'yyyy-mm' && onClick && onClick(newDate);
}}
>
);
})}
>
)}
{isFormat === 'yyyy-mm-dd' && (
<>
setIsFormat('yyyy-mm')}
title={`${currentYear}년 ${currentMonth + 1}월`}
/>
{daysOfWeek.map(day => (
{day}
))}
{calendarDays.map((day, index) => {
const date = new Date(currentYear, currentMonth, day ?? 0);
const dayOfWeek = date.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const selectable = day && (!minDate || date >= minDate) && (!maxDate || date <= maxDate);
const isToday = day === todayDate && currentMonth === todayMonth && currentYear === todayYear;
return (
selectable && day && selectDay(day)}>
);
})}
>
)}
);
};
export default Calendar;
// themes
const flexT: Interpolation = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
whiteSpace: 'nowrap',
userSelect: 'none',
textAlign: 'center',
fontSize: '0.938rem',
transition: 'background-color 0.3s ease',
};
//
const CalendarGrid = ({ ea, children }: { ea: number; children: ReactNode }) => {
return (
{children}
);
};
//
const Wrapper = ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => (
{children}
);
//
const DayOfWeek = ({ children }: { children: ReactNode } & HTMLAttributes) => (
{children}
);
//
const YYBox = ({
year,
isToday,
isSelected,
selectable,
}: {
year: number;
isToday?: boolean;
selectable?: boolean;
isSelected?: boolean;
}) => {
const colorTheme = () => {
if (isSelected) return '#fff';
if (!selectable) return '#bbb';
if (isToday) return '#4788f4';
return '#66666a';
};
const bgTheme = () => {
if (isSelected) return colors.keyColor;
if (isToday && !selectable) return '#E5EDF8';
if (isToday) return '#E5EDF8';
return '';
};
return (
{year}년
);
};
//
const MMBox = ({
month,
isToday,
selectable,
isSelectedMonth,
}: {
month: number;
isToday: boolean;
selectable: boolean;
isSelectedMonth: boolean;
}) => {
const colorTheme = () => {
if (isSelectedMonth) return '#fff';
if (!selectable) return '#bbb';
if (isToday) return '#4788f4';
return '#66666a';
};
const bgTheme = () => {
if (isSelectedMonth) return colors.keyColor;
if (isToday && !selectable) return '#E5EDF8';
if (isToday) return '#E5EDF8';
return '';
};
return (
{month + 1}월
);
};
const DDBox = ({
day,
isToday,
isWeekend,
selectable,
selectWrap,
size = 32,
}: {
day: number | null;
isToday?: any;
isWeekend?: any;
selectable?: any;
selectWrap?: any;
size?: number;
}) => {
const colorTheme = () => {
if (!!isToday && !!selectWrap) return '#fff';
if (!!isWeekend && !!selectWrap) return '#fff';
if (!!isWeekend && !selectable) return '#bbb';
if (isToday) return '#3F87FF';
if (isWeekend) return '#997F8F';
if (!selectable) return '#bbb';
if (selectWrap) return '#fff';
return '#66666a';
};
const bgTheme = () => {
if (selectWrap) return colors.keyColor;
if (isToday) return '#E5EDF8';
return '';
};
return (
{day}
);
};