/** * ScheduleCalendar 유틸리티 함수 */ import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, startOfDay, eachDayOfInterval, isSameMonth, isSameDay, isToday, isBefore, format, addMonths, subMonths, parseISO, isWithinInterval, differenceInDays, addDays, getDay, } from 'date-fns'; import { ko } from 'date-fns/locale'; import type { ScheduleEvent, DayBadge } from './types'; /** * 월간 달력에 표시할 날짜 배열 생성 * (이전 달/다음 달 날짜 포함) */ export function getMonthDays(date: Date, weekStartsOn: 0 | 1 = 0): Date[] { const monthStart = startOfMonth(date); const monthEnd = endOfMonth(date); const calendarStart = startOfWeek(monthStart, { weekStartsOn }); const calendarEnd = endOfWeek(monthEnd, { weekStartsOn }); return eachDayOfInterval({ start: calendarStart, end: calendarEnd }); } /** * 주간 달력에 표시할 날짜 배열 생성 */ export function getWeekDays(date: Date, weekStartsOn: 0 | 1 = 0): Date[] { const weekStart = startOfWeek(date, { weekStartsOn }); const weekEnd = endOfWeek(date, { weekStartsOn }); return eachDayOfInterval({ start: weekStart, end: weekEnd }); } /** * 요일 헤더 배열 생성 */ export function getWeekdayHeaders(weekStartsOn: 0 | 1 = 0): string[] { const weekdays = ['일', '월', '화', '수', '목', '금', '토']; if (weekStartsOn === 1) { return [...weekdays.slice(1), weekdays[0]]; } return weekdays; } /** * 날짜가 현재 월에 속하는지 확인 */ export function isCurrentMonth(date: Date, currentDate: Date): boolean { return isSameMonth(date, currentDate); } /** * 날짜가 오늘인지 확인 */ export function checkIsToday(date: Date): boolean { return isToday(date); } /** * 날짜가 오늘 이전인지 확인 (지난 일자) */ export function checkIsPast(date: Date): boolean { const today = startOfDay(new Date()); const targetDate = startOfDay(date); return isBefore(targetDate, today); } /** * 두 날짜가 같은지 확인 */ export function isSameDate(date1: Date | null, date2: Date): boolean { if (!date1) return false; return isSameDay(date1, date2); } /** * 날짜 포맷 */ export function formatCalendarDate(date: Date, formatStr: string = 'yyyy-MM-dd'): string { return format(date, formatStr, { locale: ko }); } /** * 년월 포맷 (예: "2025년 12월") */ export function formatYearMonth(date: Date): string { return format(date, 'yyyy년 M월', { locale: ko }); } /** * 다음 달로 이동 */ export function getNextMonth(date: Date): Date { return addMonths(date, 1); } /** * 이전 달로 이동 */ export function getPrevMonth(date: Date): Date { return subMonths(date, 1); } /** * 특정 날짜에 해당하는 이벤트 필터링 */ export function getEventsForDate(events: ScheduleEvent[], date: Date): ScheduleEvent[] { const dateStr = format(date, 'yyyy-MM-dd'); return events.filter((event) => { const startDate = parseISO(event.startDate); const endDate = parseISO(event.endDate); const targetDate = parseISO(dateStr); return isWithinInterval(targetDate, { start: startDate, end: endDate }); }); } /** * 특정 날짜에 해당하는 뱃지 찾기 */ export function getBadgeForDate(badges: DayBadge[], date: Date): DayBadge | undefined { const dateStr = format(date, 'yyyy-MM-dd'); return badges.find((badge) => badge.date === dateStr); } /** * 이벤트가 특정 날짜에 시작하는지 확인 */ export function isEventStart(event: ScheduleEvent, date: Date): boolean { return isSameDay(parseISO(event.startDate), date); } /** * 이벤트가 특정 날짜에 끝나는지 확인 */ export function isEventEnd(event: ScheduleEvent, date: Date): boolean { return isSameDay(parseISO(event.endDate), date); } /** * 이벤트 기간 (일수) */ export function getEventDuration(event: ScheduleEvent): number { const start = parseISO(event.startDate); const end = parseISO(event.endDate); return differenceInDays(end, start) + 1; } /** * 주 단위로 이벤트 분할 (여러 주에 걸치는 이벤트 처리) */ export interface WeekEventSegment { event: ScheduleEvent; startCol: number; // 0-6 (시작 컬럼) colSpan: number; // 차지하는 컬럼 수 isStart: boolean; // 이벤트 시작 주인지 isEnd: boolean; // 이벤트 끝 주인지 } export function getEventSegmentsForWeek( events: ScheduleEvent[], weekDays: Date[], weekStartsOn: 0 | 1 = 0 ): WeekEventSegment[] { const weekStart = weekDays[0]; const weekEnd = weekDays[6]; const segments: WeekEventSegment[] = []; events.forEach((event) => { const eventStart = parseISO(event.startDate); const eventEnd = parseISO(event.endDate); // 이 주와 겹치는지 확인 if (eventEnd < weekStart || eventStart > weekEnd) { return; // 이 주와 겹치지 않음 } // 이 주에서의 시작/끝 날짜 계산 const segmentStart = eventStart < weekStart ? weekStart : eventStart; const segmentEnd = eventEnd > weekEnd ? weekEnd : eventEnd; // 컬럼 위치 계산 const startCol = getDay(segmentStart); const adjustedStartCol = weekStartsOn === 1 ? (startCol === 0 ? 6 : startCol - 1) : startCol; const colSpan = differenceInDays(segmentEnd, segmentStart) + 1; segments.push({ event, startCol: adjustedStartCol, colSpan, isStart: isSameDay(eventStart, segmentStart), isEnd: isSameDay(eventEnd, segmentEnd), }); }); return segments; } /** * 이벤트 행 배치 계산 (다른 색상은 다른 행에 배치) * - 같은 색상(작업반장)끼리만 같은 행 공유 가능 * - 다른 색상은 무조건 다른 행에 배치 * - 시작일이 빠른 색상 그룹이 위에 배치 */ export function assignEventRows(segments: WeekEventSegment[]): Map { const rowMap = new Map(); // 색상별로 그룹화 const colorGroups = new Map(); segments.forEach((segment) => { const color = segment.event.color || 'blue'; if (!colorGroups.has(color)) { colorGroups.set(color, []); } colorGroups.get(color)!.push(segment); }); // 색상 그룹을 가장 이른 시작일 기준으로 정렬 const sortedColorGroups = Array.from(colorGroups.entries()).sort((a, b) => { // 각 그룹에서 가장 이른 시작일 찾기 const aMinStart = Math.min(...a[1].map(s => parseISO(s.event.startDate).getTime())); const bMinStart = Math.min(...b[1].map(s => parseISO(s.event.startDate).getTime())); return aMinStart - bMinStart; }); let currentBaseRow = 0; // 각 색상 그룹별로 행 배치 (시작일 순) sortedColorGroups.forEach(([, groupSegments]) => { const occupied: boolean[][] = []; // 시작 컬럼 순으로 정렬 const sortedSegments = [...groupSegments].sort((a, b) => { if (a.startCol !== b.startCol) return a.startCol - b.startCol; return b.colSpan - a.colSpan; // 긴 이벤트 먼저 }); let maxRowInGroup = 0; sortedSegments.forEach((segment) => { const { event, startCol, colSpan } = segment; // 이 색상 그룹 내에서 가능한 가장 낮은 행 찾기 let row = 0; while (true) { if (!occupied[row]) { occupied[row] = Array(7).fill(false); } // 이 행에서 해당 컬럼들이 비어있는지 확인 let canPlace = true; for (let col = startCol; col < startCol + colSpan && col < 7; col++) { if (occupied[row][col]) { canPlace = false; break; } } if (canPlace) { // 배치 for (let col = startCol; col < startCol + colSpan && col < 7; col++) { occupied[row][col] = true; } rowMap.set(event.id, currentBaseRow + row); maxRowInGroup = Math.max(maxRowInGroup, row); break; } row++; if (row > 10) break; // 최대 10행까지 } }); // 다음 색상 그룹은 이 그룹 아래 행부터 시작 currentBaseRow += maxRowInGroup + 1; }); return rowMap; } /** * 전역 이벤트 행 배치 계산 (월간 뷰 전체 기준) * - 먼저 시작한 이벤트가 끝날 때까지 같은 행 유지 * - 나중에 시작한 이벤트는 아래 행으로 배치 */ export function assignGlobalEventRows(events: ScheduleEvent[]): Map { const rowMap = new Map(); if (events.length === 0) return rowMap; // 모든 이벤트를 시작일 순으로 정렬 (시작일 같으면 기간 긴 것 먼저) const sortedEvents = [...events].sort((a, b) => { const aStart = parseISO(a.startDate).getTime(); const bStart = parseISO(b.startDate).getTime(); if (aStart !== bStart) return aStart - bStart; const aDuration = parseISO(a.endDate).getTime() - aStart; const bDuration = parseISO(b.endDate).getTime() - bStart; return bDuration - aDuration; }); // 각 행의 점유 종료일 추적 const rowEndDates: number[] = []; sortedEvents.forEach((event) => { const eventStart = parseISO(event.startDate).getTime(); const eventEnd = parseISO(event.endDate).getTime(); // 이 이벤트를 배치할 수 있는 가장 낮은 행 찾기 let row = 0; while (row < rowEndDates.length) { // 이전 이벤트가 끝났으면 배치 가능 if (rowEndDates[row] < eventStart) { break; } row++; } // 행에 배치하고 종료일 업데이트 rowEndDates[row] = eventEnd; rowMap.set(event.id, row); }); return rowMap; } /** * 다음 날로 이동 */ export function getNextDay(date: Date): Date { return addDays(date, 1); } /** * 이전 날로 이동 */ export function getPrevDay(date: Date): Date { return addDays(date, -1); } /** * 다음 주로 이동 */ export function getNextWeek(date: Date): Date { return addDays(date, 7); } /** * 이전 주로 이동 */ export function getPrevWeek(date: Date): Date { return addDays(date, -7); } /** * 년월일 포맷 (예: "2026년 2월 2일 (월)") */ export function formatYearMonthDay(date: Date): string { return format(date, 'yyyy년 M월 d일 (EEE)', { locale: ko }); } /** * 월간 뷰에서 주 단위로 날짜 분할 */ export function splitIntoWeeks(days: Date[]): Date[][] { const weeks: Date[][] = []; for (let i = 0; i < days.length; i += 7) { weeks.push(days.slice(i, i + 7)); } return weeks; }