- MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선 - DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가 - useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱 - 전 도메인 날짜 필드 DatePicker 표준화 - 생산대시보드/작업지시/견적서/주문관리 모바일 호환성 강화 - 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등) - 달력 일정 관리 API 연동 및 대량 등록 다이얼로그 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
6.3 KiB
TypeScript
203 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { cn } from '@/components/ui/utils';
|
|
import { CalendarHeader } from './CalendarHeader';
|
|
import { DayTimeView } from './DayTimeView';
|
|
import { MonthView } from './MonthView';
|
|
import { WeekView } from './WeekView';
|
|
import { WeekTimeView } from './WeekTimeView';
|
|
import type { ScheduleCalendarProps, CalendarView } from './types';
|
|
import { getNextMonth, getPrevMonth, getNextDay, getPrevDay, getNextWeek, getPrevWeek } from './utils';
|
|
import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore';
|
|
|
|
/**
|
|
* 스케줄 달력 공통 컴포넌트
|
|
*
|
|
* 주/월 뷰 전환, 일정 바 표시, 날짜별 뱃지 등을 지원하는 재사용 가능한 달력
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <ScheduleCalendar
|
|
* events={[
|
|
* { id: '1', title: '김담당 - 현장A / ORD-001', startDate: '2025-12-01', endDate: '2025-12-05', color: 'blue' },
|
|
* ]}
|
|
* badges={[
|
|
* { date: '2025-12-01', count: 3, color: 'red' },
|
|
* ]}
|
|
* onDateClick={(date) => console.log('날짜 클릭:', date)}
|
|
* onEventClick={(event) => console.log('이벤트 클릭:', event)}
|
|
* filterSlot={<Select>...</Select>}
|
|
* />
|
|
* ```
|
|
*/
|
|
export function ScheduleCalendar({
|
|
events = [],
|
|
badges = [],
|
|
currentDate: controlledDate,
|
|
view: controlledView,
|
|
selectedDate: controlledSelectedDate,
|
|
onDateClick,
|
|
onEventClick,
|
|
onMonthChange,
|
|
onViewChange,
|
|
titleSlot,
|
|
filterSlot,
|
|
maxEventsPerDay = 5,
|
|
weekStartsOn = 0,
|
|
isLoading = false,
|
|
className,
|
|
availableViews,
|
|
timeRange,
|
|
hideNavigation,
|
|
}: ScheduleCalendarProps) {
|
|
// 내부 상태 (controlled/uncontrolled 지원)
|
|
const [internalDate, setInternalDate] = useState(() => new Date());
|
|
const [internalView, setInternalView] = useState<CalendarView>('month');
|
|
const [internalSelectedDate, setInternalSelectedDate] = useState<Date | null>(null);
|
|
|
|
// 현재 사용할 값 결정
|
|
const currentDate = controlledDate ?? internalDate;
|
|
const view = controlledView ?? internalView;
|
|
const selectedDate = controlledSelectedDate !== undefined ? controlledSelectedDate : internalSelectedDate;
|
|
|
|
// Hydration 불일치 방지
|
|
const [mounted, setMounted] = useState(false);
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
// 현재 표시 중인 연도의 공휴일/일정 데이터를 스토어에 로드
|
|
useEffect(() => {
|
|
const year = currentDate.getFullYear();
|
|
useCalendarScheduleStore.getState().fetchSchedules(year);
|
|
}, [currentDate]);
|
|
|
|
// 이전 (뷰에 따라 일/주/월 단위)
|
|
const handlePrevMonth = useCallback(() => {
|
|
let newDate: Date;
|
|
if (view === 'day-time') {
|
|
newDate = getPrevDay(currentDate);
|
|
} else if (view === 'week-time' || view === 'week') {
|
|
newDate = getPrevWeek(currentDate);
|
|
} else {
|
|
newDate = getPrevMonth(currentDate);
|
|
}
|
|
if (controlledDate === undefined) {
|
|
setInternalDate(newDate);
|
|
}
|
|
onMonthChange?.(newDate);
|
|
}, [currentDate, view, controlledDate, onMonthChange]);
|
|
|
|
// 다음 (뷰에 따라 일/주/월 단위)
|
|
const handleNextMonth = useCallback(() => {
|
|
let newDate: Date;
|
|
if (view === 'day-time') {
|
|
newDate = getNextDay(currentDate);
|
|
} else if (view === 'week-time' || view === 'week') {
|
|
newDate = getNextWeek(currentDate);
|
|
} else {
|
|
newDate = getNextMonth(currentDate);
|
|
}
|
|
if (controlledDate === undefined) {
|
|
setInternalDate(newDate);
|
|
}
|
|
onMonthChange?.(newDate);
|
|
}, [currentDate, view, controlledDate, onMonthChange]);
|
|
|
|
// 뷰 변경
|
|
const handleViewChange = useCallback((newView: CalendarView) => {
|
|
if (controlledView === undefined) {
|
|
setInternalView(newView);
|
|
}
|
|
onViewChange?.(newView);
|
|
}, [controlledView, onViewChange]);
|
|
|
|
// 날짜 클릭
|
|
const handleDateClick = useCallback((date: Date) => {
|
|
if (controlledSelectedDate === undefined) {
|
|
setInternalSelectedDate(date);
|
|
}
|
|
onDateClick?.(date);
|
|
}, [controlledSelectedDate, onDateClick]);
|
|
|
|
// 이벤트 클릭
|
|
const handleEventClick = useCallback((event: import('./types').ScheduleEvent) => {
|
|
onEventClick?.(event);
|
|
}, [onEventClick]);
|
|
|
|
// SSR에서는 빈 컨테이너 렌더링
|
|
if (!mounted) {
|
|
return (
|
|
<div className={cn('min-h-[400px] rounded-md border p-4', className)} />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={cn('rounded-md border p-4 bg-background', className)}>
|
|
{/* 헤더 */}
|
|
<CalendarHeader
|
|
currentDate={currentDate}
|
|
view={view}
|
|
onPrevMonth={handlePrevMonth}
|
|
onNextMonth={handleNextMonth}
|
|
onViewChange={handleViewChange}
|
|
titleSlot={titleSlot}
|
|
filterSlot={filterSlot}
|
|
availableViews={availableViews}
|
|
hideNavigation={hideNavigation}
|
|
/>
|
|
|
|
{/* 본문 */}
|
|
<div className="mt-4">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center min-h-[300px]">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
</div>
|
|
) : view === 'day-time' ? (
|
|
<DayTimeView
|
|
currentDate={currentDate}
|
|
events={events}
|
|
selectedDate={selectedDate}
|
|
timeRange={timeRange}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
/>
|
|
) : view === 'week-time' ? (
|
|
<WeekTimeView
|
|
currentDate={currentDate}
|
|
events={events}
|
|
selectedDate={selectedDate}
|
|
weekStartsOn={weekStartsOn}
|
|
timeRange={timeRange}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
/>
|
|
) : view === 'month' ? (
|
|
<MonthView
|
|
currentDate={currentDate}
|
|
events={events}
|
|
badges={badges}
|
|
selectedDate={selectedDate}
|
|
maxEventsPerDay={maxEventsPerDay}
|
|
weekStartsOn={weekStartsOn}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
/>
|
|
) : (
|
|
<WeekView
|
|
currentDate={currentDate}
|
|
events={events}
|
|
badges={badges}
|
|
selectedDate={selectedDate}
|
|
maxEventsPerDay={maxEventsPerDay}
|
|
weekStartsOn={weekStartsOn}
|
|
onDateClick={handleDateClick}
|
|
onEventClick={handleEventClick}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|