Files
sam-react-prod/src/components/common/ScheduleCalendar/utils.ts

393 lines
10 KiB
TypeScript
Raw Normal View History

/**
* 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<string, number> {
const rowMap = new Map<string, number>();
// 색상별로 그룹화
const colorGroups = new Map<string, WeekEventSegment[]>();
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<string, number> {
const rowMap = new Map<string, number>();
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;
}