feat(WEB): 입찰/계약/주문관리 기능 추가 및 견적 상세 리팩토링
- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터 - 계약관리: 목록/상세/수정 페이지 구현 - 주문관리: 수주/발주 목록 및 상세 페이지 구현 - 견적 상세 폼: 섹션별 분리 및 hooks/utils 리팩토링 - 품목관리, 카테고리관리, 단가관리 기능 추가 - 현장설명회/협력업체 폼 개선 - 프린트 유틸리티 공통화 (print-utils.ts) - 문서 모달 공통 컴포넌트 정리 - IntegratedListTemplateV2, StatCards 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
292
src/components/common/ScheduleCalendar/utils.ts
Normal file
292
src/components/common/ScheduleCalendar/utils.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* ScheduleCalendar 유틸리티 함수
|
||||
*/
|
||||
|
||||
import {
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
eachDayOfInterval,
|
||||
isSameMonth,
|
||||
isSameDay,
|
||||
isToday,
|
||||
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 isSameDate(date1: Date | null, date2: Date): boolean {
|
||||
if (!date1) return false;
|
||||
return isSameDay(date1, date2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 포맷
|
||||
*/
|
||||
export function formatDate(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);
|
||||
});
|
||||
|
||||
let currentBaseRow = 0;
|
||||
|
||||
// 각 색상 그룹별로 행 배치
|
||||
colorGroups.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 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;
|
||||
}
|
||||
Reference in New Issue
Block a user