From eccfd959fe2f8d49078951e848b8480cd7d25cad Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Tue, 6 Jan 2026 11:03:33 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20React/Next.js=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=20=EC=BA=98?= =?UTF-8?q?=EB=A6=B0=EB=8D=94/=EC=A3=BC=EB=AC=B8=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보안: next 15.5.7 → 15.5.9 (CVE-2025-55184, CVE-2025-55183, CVE-2025-67779) - 보안: react/react-dom 19.2.1 → 19.2.3 - 캘린더: MonthView, ScheduleBar 개선 - 주문관리: 리스트/액션/타입 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 34 ++++----- package.json | 6 +- .../OrderManagementListClient.tsx | 5 +- .../business/juil/order-management/actions.ts | 49 ++++++------ .../business/juil/order-management/types.ts | 63 +++++++++++++++- .../common/ScheduleCalendar/MonthView.tsx | 24 +++--- .../common/ScheduleCalendar/ScheduleBar.tsx | 75 ++++++++++++------- .../common/ScheduleCalendar/types.ts | 11 ++- .../common/ScheduleCalendar/utils.ts | 58 +++++++++++++- 9 files changed, 241 insertions(+), 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index ada4b46f..04168ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,11 +41,11 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.552.0", - "next": "^15.5.7", + "next": "^15.5.9", "next-intl": "^4.4.0", - "react": "^19.2.1", + "react": "^19.2.3", "react-day-picker": "^9.11.1", - "react-dom": "^19.2.1", + "react-dom": "^19.2.3", "react-hook-form": "^7.66.0", "recharts": "^3.4.1", "sonner": "^2.0.7", @@ -944,9 +944,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -8011,12 +8011,12 @@ } }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -8702,9 +8702,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8732,15 +8732,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.1" + "react": "^19.2.3" } }, "node_modules/react-hook-form": { diff --git a/package.json b/package.json index 1b36a3df..4038c0c6 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,11 @@ "cmdk": "^1.1.1", "date-fns": "^4.1.0", "lucide-react": "^0.552.0", - "next": "^15.5.7", + "next": "^15.5.9", "next-intl": "^4.4.0", - "react": "^19.2.1", + "react": "^19.2.3", "react-day-picker": "^9.11.1", - "react-dom": "^19.2.1", + "react-dom": "^19.2.3", "react-hook-form": "^7.66.0", "recharts": "^3.4.1", "sonner": "^2.0.7", diff --git a/src/components/business/juil/order-management/OrderManagementListClient.tsx b/src/components/business/juil/order-management/OrderManagementListClient.tsx index 856ab623..2a7ee674 100644 --- a/src/components/business/juil/order-management/OrderManagementListClient.tsx +++ b/src/components/business/juil/order-management/OrderManagementListClient.tsx @@ -36,7 +36,6 @@ import { ORDER_SORT_OPTIONS, ORDER_STATUS_STYLES, ORDER_STATUS_LABELS, - ORDER_STATUS_CALENDAR_COLORS, ORDER_TYPE_OPTIONS, ORDER_TYPE_LABELS, MOCK_PARTNERS, @@ -45,6 +44,7 @@ import { MOCK_ORDER_MANAGERS, MOCK_ORDER_COMPANIES, MOCK_WORK_TEAM_LEADERS, + getScheduleColorByManager, } from './types'; import { getOrderList, @@ -155,6 +155,7 @@ export default function OrderManagementListClient({ const workTeamOptions: MultiSelectOption[] = useMemo(() => MOCK_WORK_TEAM_LEADERS, []); // 달력용 이벤트 데이터 변환 (필터 적용) + // 색상: 담당자(orderManager)별 고정 색상 const calendarEvents: ScheduleEvent[] = useMemo(() => { return orders .filter((order) => { @@ -183,7 +184,7 @@ export default function OrderManagementListClient({ title: `${order.orderManager} - ${order.siteName} / ${order.orderNumber}`, startDate: order.periodStart, endDate: order.periodEnd, - color: ORDER_STATUS_CALENDAR_COLORS[order.status], + color: getScheduleColorByManager(order.orderManager), // 담당자별 고정 색상 status: order.status, data: order, })); diff --git a/src/components/business/juil/order-management/actions.ts b/src/components/business/juil/order-management/actions.ts index ede9dcd5..d006af86 100644 --- a/src/components/business/juil/order-management/actions.ts +++ b/src/components/business/juil/order-management/actions.ts @@ -5,8 +5,9 @@ import { MOCK_ORDER_DETAIL } from './types'; import { format, addDays, subDays, subMonths } from 'date-fns'; /** - * 목업 발주 데이터 생성 + * 목업 발주 데이터 생성 (고정 데이터) * - types.ts의 MOCK 옵션들과 정확히 일치해야 필터가 동작함 + * - Math.random() 제거 → index 기반 deterministic 데이터 */ function generateMockOrders(): Order[] { // types.ts MOCK_PARTNERS와 일치 @@ -61,20 +62,26 @@ function generateMockOrders(): Order[] { const statuses: Order['status'][] = ['waiting', 'order_complete', 'delivery_scheduled', 'delivery_complete']; const orders: Order[] = []; - const today = new Date(); + // 고정 기준일 (2026-01-06) + const baseDate = new Date(2026, 0, 6); for (let i = 0; i < 50; i++) { - const partner = partners[Math.floor(Math.random() * partners.length)]; - const site = sites[Math.floor(Math.random() * sites.length)]; - const status = statuses[Math.floor(Math.random() * statuses.length)]; - const orderType = orderTypes[Math.floor(Math.random() * orderTypes.length)]; - const periodStart = subMonths(today, Math.floor(Math.random() * 3)); - const periodEnd = addDays(periodStart, Math.floor(Math.random() * 30) + 10); - const orderDate = subDays(periodStart, Math.floor(Math.random() * 5)); - const constructionStartDate = addDays(periodStart, Math.floor(Math.random() * 5)); - const plannedDelivery = addDays(orderDate, Math.floor(Math.random() * 14) + 3); + // index 기반 deterministic 선택 (랜덤 제거) + const partner = partners[i % partners.length]; + const site = sites[i % sites.length]; + const status = statuses[i % statuses.length]; + const orderType = orderTypes[i % orderTypes.length]; + + // 날짜도 index 기반으로 고정 + const monthOffset = i % 3; // 0, 1, 2개월 전 + const dayOffset = (i * 3) % 30; // 0~29일 분산 + const periodStart = subMonths(addDays(baseDate, -dayOffset), monthOffset); + const periodEnd = addDays(periodStart, 10 + (i % 20)); // 10~29일 기간 + const orderDate = subDays(periodStart, i % 5); + const constructionStartDate = addDays(periodStart, i % 5); + const plannedDelivery = addDays(orderDate, 3 + (i % 14)); const actualDelivery = status === 'delivery_complete' - ? format(addDays(plannedDelivery, Math.floor(Math.random() * 5) - 2), 'yyyy-MM-dd') + ? format(addDays(plannedDelivery, (i % 5) - 2), 'yyyy-MM-dd') : null; orders.push({ @@ -83,24 +90,24 @@ function generateMockOrders(): Order[] { partnerId: partner.id, partnerName: partner.name, siteName: site, - name: names[Math.floor(Math.random() * names.length)], - constructionPM: constructionPMs[Math.floor(Math.random() * constructionPMs.length)], - orderManager: orderManagers[Math.floor(Math.random() * orderManagers.length)], + name: names[i % names.length], + constructionPM: constructionPMs[i % constructionPMs.length], + orderManager: orderManagers[i % orderManagers.length], orderNumber: `ORD-${2026}-${String(i + 1).padStart(4, '0')}`, - orderCompany: orderCompanies[Math.floor(Math.random() * orderCompanies.length)], - workTeamLeader: workTeamLeaders[Math.floor(Math.random() * workTeamLeaders.length)], + orderCompany: orderCompanies[i % orderCompanies.length], + workTeamLeader: workTeamLeaders[i % workTeamLeaders.length], constructionStartDate: format(constructionStartDate, 'yyyy-MM-dd'), orderType, - item: items[Math.floor(Math.random() * items.length)], - quantity: Math.floor(Math.random() * 100) + 1, + item: items[i % items.length], + quantity: 10 + (i * 7) % 90, // 10~99 고정 패턴 orderDate: format(orderDate, 'yyyy-MM-dd'), plannedDeliveryDate: format(plannedDelivery, 'yyyy-MM-dd'), actualDeliveryDate: actualDelivery, status, periodStart: format(periodStart, 'yyyy-MM-dd'), periodEnd: format(periodEnd, 'yyyy-MM-dd'), - createdAt: format(subDays(periodStart, Math.floor(Math.random() * 10)), 'yyyy-MM-dd\'T\'HH:mm:ss'), - updatedAt: format(today, 'yyyy-MM-dd\'T\'HH:mm:ss'), + createdAt: format(subDays(periodStart, i % 10), 'yyyy-MM-dd\'T\'HH:mm:ss'), + updatedAt: format(baseDate, 'yyyy-MM-dd\'T\'HH:mm:ss'), }); } diff --git a/src/components/business/juil/order-management/types.ts b/src/components/business/juil/order-management/types.ts index 40c30eea..c73c65c7 100644 --- a/src/components/business/juil/order-management/types.ts +++ b/src/components/business/juil/order-management/types.ts @@ -112,7 +112,7 @@ export const ORDER_STATUS_STYLES: Record = { }; /** - * 발주 상태별 달력 색상 + * 발주 상태별 달력 색상 (레거시 - 상태 기반) */ export const ORDER_STATUS_CALENDAR_COLORS: Record = { waiting: 'yellow', @@ -121,6 +121,67 @@ export const ORDER_STATUS_CALENDAR_COLORS: Record = { delivery_complete: 'green', }; +/** + * 스케줄 색상 팔레트 (10가지 고정 색상) + * - 인덱스 기반 순환: 11번째 이벤트는 다시 1번 색상 + * - API 연동 시에도 동일하게 사용 + */ +export const SCHEDULE_COLOR_PALETTE = [ + { name: 'blue', bg: 'bg-blue-500', text: 'text-white', hex: '#3b82f6' }, + { name: 'green', bg: 'bg-green-500', text: 'text-white', hex: '#22c55e' }, + { name: 'yellow', bg: 'bg-yellow-500', text: 'text-white', hex: '#eab308' }, + { name: 'red', bg: 'bg-red-500', text: 'text-white', hex: '#ef4444' }, + { name: 'purple', bg: 'bg-purple-500', text: 'text-white', hex: '#a855f7' }, + { name: 'pink', bg: 'bg-pink-500', text: 'text-white', hex: '#ec4899' }, + { name: 'orange', bg: 'bg-orange-500', text: 'text-white', hex: '#f97316' }, + { name: 'teal', bg: 'bg-teal-500', text: 'text-white', hex: '#14b8a6' }, + { name: 'indigo', bg: 'bg-indigo-500', text: 'text-white', hex: '#6366f1' }, + { name: 'cyan', bg: 'bg-cyan-500', text: 'text-white', hex: '#06b6d4' }, +] as const; + +/** + * 인덱스 기반 스케줄 색상 반환 (순환) + * @param index - 이벤트 인덱스 (0부터 시작) + * @returns 색상 이름 (ScheduleCalendar에서 사용) + */ +export function getScheduleColor(index: number): string { + return SCHEDULE_COLOR_PALETTE[index % SCHEDULE_COLOR_PALETTE.length].name; +} + +/** + * 담당자별 색상 매핑 + * - 같은 담당자는 항상 같은 색상 + * - MOCK_ORDER_MANAGERS 순서에 따라 색상 할당 + */ +const ORDER_MANAGER_COLOR_MAP: Record = { + '김담당': 'blue', + '이담당': 'green', + '박담당': 'pink', + '최담당': 'purple', +}; + +/** + * 담당자 이름 기반 스케줄 색상 반환 + * - 매핑된 담당자: 고정 색상 + * - 매핑되지 않은 담당자: 이름 해시 기반 색상 (일관성 유지) + * @param managerName - 발주담당자 이름 + * @returns 색상 이름 (ScheduleCalendar에서 사용) + */ +export function getScheduleColorByManager(managerName: string): string { + // 매핑된 담당자면 고정 색상 반환 + if (ORDER_MANAGER_COLOR_MAP[managerName]) { + return ORDER_MANAGER_COLOR_MAP[managerName]; + } + + // 매핑되지 않은 담당자: 이름 해시 기반으로 일관된 색상 할당 + let hash = 0; + for (let i = 0; i < managerName.length; i++) { + hash = managerName.charCodeAt(i) + ((hash << 5) - hash); + } + const index = Math.abs(hash) % SCHEDULE_COLOR_PALETTE.length; + return SCHEDULE_COLOR_PALETTE[index].name; +} + /** * 발주 구분 (타입) 옵션 */ diff --git a/src/components/common/ScheduleCalendar/MonthView.tsx b/src/components/common/ScheduleCalendar/MonthView.tsx index 6debf646..f8cb071d 100644 --- a/src/components/common/ScheduleCalendar/MonthView.tsx +++ b/src/components/common/ScheduleCalendar/MonthView.tsx @@ -14,7 +14,7 @@ import { isSameDate, splitIntoWeeks, getEventSegmentsForWeek, - assignEventRows, + assignGlobalEventRows, getEventsForDate, getBadgeForDate, } from './utils'; @@ -53,6 +53,12 @@ export function MonthView({ // 주 단위로 분할 const weeks = useMemo(() => splitIntoWeeks(monthDays), [monthDays]); + // 전역 row 할당 (먼저 시작한 이벤트가 종료까지 같은 행 유지) + const globalRowAssignments = useMemo( + () => assignGlobalEventRows(events), + [events] + ); + return (
{/* 요일 헤더 */} @@ -89,6 +95,7 @@ export function MonthView({ selectedDate={selectedDate} maxEventsPerDay={maxEventsPerDay} weekStartsOn={weekStartsOn} + globalRowAssignments={globalRowAssignments} onDateClick={onDateClick} onEventClick={onEventClick} /> @@ -106,6 +113,7 @@ interface WeekRowProps { selectedDate: Date | null; maxEventsPerDay: number; weekStartsOn: 0 | 1; + globalRowAssignments: Map; onDateClick: (date: Date) => void; onEventClick: (event: import('./types').ScheduleEvent) => void; } @@ -118,6 +126,7 @@ function WeekRow({ selectedDate, maxEventsPerDay, weekStartsOn, + globalRowAssignments, onDateClick, onEventClick, }: WeekRowProps) { @@ -127,12 +136,6 @@ function WeekRow({ [events, weekDays, weekStartsOn] ); - // 이벤트 행 배치 - const rowAssignments = useMemo( - () => assignEventRows(eventSegments), - [eventSegments] - ); - // 표시할 이벤트 수 계산 (maxEventsPerDay 초과 시 +N 표시) const visibleRows = maxEventsPerDay; @@ -152,7 +155,8 @@ function WeekRow({ }, [weekDays, events, visibleRows]); // 셀 최소 높이 계산 (이벤트 행 수에 따라) - 더 넉넉하게 확보 - const maxRowIndex = Math.max(0, ...Array.from(rowAssignments.values())); + const segmentRowIndices = eventSegments.map(s => globalRowAssignments.get(s.event.id) || 0); + const maxRowIndex = Math.max(0, ...segmentRowIndices); const rowHeight = Math.max(120, 40 + Math.min(maxRowIndex + 1, visibleRows) * 28 + 24); return ( @@ -212,11 +216,11 @@ function WeekRow({ {/* 이벤트 바들 (절대 위치) */} {eventSegments .filter((segment) => { - const rowIndex = rowAssignments.get(segment.event.id) || 0; + const rowIndex = globalRowAssignments.get(segment.event.id) || 0; return rowIndex < visibleRows; }) .map((segment) => { - const rowIndex = rowAssignments.get(segment.event.id) || 0; + const rowIndex = globalRowAssignments.get(segment.event.id) || 0; return ( { - e.stopPropagation(); - onClick(event); - }} - className={cn( - 'absolute h-5 px-2 text-xs font-medium truncate', - 'transition-all hover:opacity-80 hover:shadow-sm', - 'flex items-center cursor-pointer', - colorClass, - // 라운드 처리 - isStart && isEnd && 'rounded-md', - isStart && !isEnd && 'rounded-l-md', - !isStart && isEnd && 'rounded-r-md', - !isStart && !isEnd && 'rounded-none' - )} - style={{ - width: `calc(${widthPercent}% - 4px)`, - left: `calc(${leftPercent}% + 2px)`, - top: `${rowIndex * 24 + 32}px`, // 날짜 영역(32px) 아래부터 시작 - }} - title={event.title} - > - {isStart && {event.title}} - + + + + + + +
+

{event.title}

+

+ 기간: {event.startDate} ~ {event.endDate} +

+
+
+
+
); } \ No newline at end of file diff --git a/src/components/common/ScheduleCalendar/types.ts b/src/components/common/ScheduleCalendar/types.ts index 9cb37e65..987d5763 100644 --- a/src/components/common/ScheduleCalendar/types.ts +++ b/src/components/common/ScheduleCalendar/types.ts @@ -161,14 +161,21 @@ export interface WeekViewProps { } /** - * 이벤트 색상 매핑 + * 이벤트 색상 매핑 (10가지 색상 + gray) + * - 순환 색상: blue, green, yellow, red, purple, pink, orange, teal, indigo, cyan */ export const EVENT_COLORS: Record = { gray: 'bg-gray-400 text-white', blue: 'bg-blue-500 text-white', - yellow: 'bg-yellow-500 text-white', green: 'bg-green-500 text-white', + yellow: 'bg-yellow-500 text-white', red: 'bg-red-500 text-white', + purple: 'bg-purple-500 text-white', + pink: 'bg-pink-500 text-white', + orange: 'bg-orange-500 text-white', + teal: 'bg-teal-500 text-white', + indigo: 'bg-indigo-500 text-white', + cyan: 'bg-cyan-500 text-white', }; /** diff --git a/src/components/common/ScheduleCalendar/utils.ts b/src/components/common/ScheduleCalendar/utils.ts index 7d774f68..52f7fcb0 100644 --- a/src/components/common/ScheduleCalendar/utils.ts +++ b/src/components/common/ScheduleCalendar/utils.ts @@ -211,6 +211,7 @@ export function getEventSegmentsForWeek( * 이벤트 행 배치 계산 (다른 색상은 다른 행에 배치) * - 같은 색상(작업반장)끼리만 같은 행 공유 가능 * - 다른 색상은 무조건 다른 행에 배치 + * - 시작일이 빠른 색상 그룹이 위에 배치 */ export function assignEventRows(segments: WeekEventSegment[]): Map { const rowMap = new Map(); @@ -225,10 +226,18 @@ export function assignEventRows(segments: WeekEventSegment[]): Map { + // 각 그룹에서 가장 이른 시작일 찾기 + 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; - // 각 색상 그룹별로 행 배치 - colorGroups.forEach((groupSegments) => { + // 각 색상 그룹별로 행 배치 (시작일 순) + sortedColorGroups.forEach(([, groupSegments]) => { const occupied: boolean[][] = []; // 시작 컬럼 순으로 정렬 @@ -280,6 +289,51 @@ export function assignEventRows(segments: WeekEventSegment[]): 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; +} + /** * 월간 뷰에서 주 단위로 날짜 분할 */