fix(WEB): React/Next.js 보안 업데이트 및 캘린더/주문관리 개선

- 보안: 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 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-06 11:03:33 +09:00
parent a938da9e22
commit eccfd959fe
9 changed files with 241 additions and 84 deletions

View File

@@ -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,
}));

View File

@@ -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'),
});
}

View File

@@ -112,7 +112,7 @@ export const ORDER_STATUS_STYLES: Record<OrderStatus, string> = {
};
/**
* 발주 상태별 달력 색상
* 발주 상태별 달력 색상 (레거시 - 상태 기반)
*/
export const ORDER_STATUS_CALENDAR_COLORS: Record<OrderStatus, string> = {
waiting: 'yellow',
@@ -121,6 +121,67 @@ export const ORDER_STATUS_CALENDAR_COLORS: Record<OrderStatus, string> = {
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<string, string> = {
'김담당': '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;
}
/**
* 발주 구분 (타입) 옵션
*/