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

@@ -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 (
<div className="flex flex-col">
{/* 요일 헤더 */}
@@ -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<string, number>;
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 (
<ScheduleBar

View File

@@ -3,6 +3,12 @@
import { cn } from '@/components/ui/utils';
import type { ScheduleEvent } from './types';
import { EVENT_COLORS } from './types';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
interface ScheduleBarProps {
event: ScheduleEvent;
@@ -25,6 +31,7 @@ interface ScheduleBarProps {
* - 여러 날에 걸치는 바 표시
* - 색상 구분 (상태별)
* - 호버/클릭 이벤트
* - 툴팁으로 담당자/기간 정보 표시
*/
export function ScheduleBar({
event,
@@ -42,32 +49,48 @@ export function ScheduleBar({
const widthPercent = (colSpan / 7) * 100;
const leftPercent = (startCol / 7) * 100;
// 툴팁 내용 생성
const tooltipContent = `${event.title}\n기간: ${event.startDate} ~ ${event.endDate}`;
return (
<button
type="button"
onClick={(e) => {
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 && <span className="truncate">{event.title}</span>}
</button>
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={(e) => {
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) 아래부터 시작
}}
>
{isStart && <span className="truncate">{event.title}</span>}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<div className="text-sm">
<p className="font-medium">{event.title}</p>
<p className="text-muted-foreground text-xs mt-1">
: {event.startDate} ~ {event.endDate}
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -161,14 +161,21 @@ export interface WeekViewProps {
}
/**
* 이벤트 색상 매핑
* 이벤트 색상 매핑 (10가지 색상 + gray)
* - 순환 색상: blue, green, yellow, red, purple, pink, orange, teal, indigo, cyan
*/
export const EVENT_COLORS: Record<string, string> = {
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',
};
/**

View File

@@ -211,6 +211,7 @@ export function getEventSegmentsForWeek(
* 이벤트 행 배치 계산 (다른 색상은 다른 행에 배치)
* - 같은 색상(작업반장)끼리만 같은 행 공유 가능
* - 다른 색상은 무조건 다른 행에 배치
* - 시작일이 빠른 색상 그룹이 위에 배치
*/
export function assignEventRows(segments: WeekEventSegment[]): Map<string, number> {
const rowMap = new Map<string, number>();
@@ -225,10 +226,18 @@ export function assignEventRows(segments: WeekEventSegment[]): Map<string, numbe
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;
// 각 색상 그룹별로 행 배치
colorGroups.forEach((groupSegments) => {
// 각 색상 그룹별로 행 배치 (시작일 순)
sortedColorGroups.forEach(([, groupSegments]) => {
const occupied: boolean[][] = [];
// 시작 컬럼 순으로 정렬
@@ -280,6 +289,51 @@ export function assignEventRows(segments: WeekEventSegment[]): Map<string, numbe
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;
}
/**
* 월간 뷰에서 주 단위로 날짜 분할
*/