ERP-node/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx

209 lines
5.8 KiB
TypeScript

"use client";
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import {
Resource,
ScheduleItem,
ZoomLevel,
TimelineSchedulerConfig,
} from "../types";
import { ScheduleBar } from "./ScheduleBar";
interface ResourceRowProps {
resource: Resource;
schedules: ScheduleItem[];
startDate: Date;
endDate: Date;
zoomLevel: ZoomLevel;
rowHeight: number;
cellWidth: number;
resourceColumnWidth: number;
config: TimelineSchedulerConfig;
/** 충돌 스케줄 ID 목록 */
conflictIds?: Set<string>;
onScheduleClick?: (schedule: ScheduleItem) => void;
onCellClick?: (resourceId: string, date: Date) => void;
/** 드래그 완료: deltaX(픽셀) 전달 */
onDragComplete?: (schedule: ScheduleItem, deltaX: number) => void;
/** 리사이즈 완료: direction + deltaX(픽셀) 전달 */
onResizeComplete?: (
schedule: ScheduleItem,
direction: "start" | "end",
deltaX: number
) => void;
}
const getDaysDiff = (start: Date, end: Date): number => {
const startTime = new Date(start).setHours(0, 0, 0, 0);
const endTime = new Date(end).setHours(0, 0, 0, 0);
return Math.round((endTime - startTime) / (1000 * 60 * 60 * 24));
};
const getCellCount = (startDate: Date, endDate: Date): number => {
return getDaysDiff(startDate, endDate) + 1;
};
export function ResourceRow({
resource,
schedules,
startDate,
endDate,
zoomLevel,
rowHeight,
cellWidth,
resourceColumnWidth,
config,
conflictIds,
onScheduleClick,
onCellClick,
onDragComplete,
onResizeComplete,
}: ResourceRowProps) {
const totalCells = useMemo(
() => getCellCount(startDate, endDate),
[startDate, endDate]
);
const gridWidth = totalCells * cellWidth;
const today = useMemo(() => {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d;
}, []);
// 스케줄 바 위치 계산
const schedulePositions = useMemo(() => {
return schedules.map((schedule) => {
const scheduleStart = new Date(schedule.startDate);
const scheduleEnd = new Date(schedule.endDate);
scheduleStart.setHours(0, 0, 0, 0);
scheduleEnd.setHours(0, 0, 0, 0);
const startOffset = getDaysDiff(startDate, scheduleStart);
const left = Math.max(0, startOffset * cellWidth);
const durationDays = getDaysDiff(scheduleStart, scheduleEnd) + 1;
const visibleStartOffset = Math.max(0, startOffset);
const visibleEndOffset = Math.min(
totalCells,
startOffset + durationDays
);
const width = Math.max(
cellWidth,
(visibleEndOffset - visibleStartOffset) * cellWidth
);
// 시작일 = 종료일이면 마일스톤
const isMilestone = schedule.startDate === schedule.endDate;
return {
schedule,
isMilestone,
position: {
left: resourceColumnWidth + left,
top: 0,
width,
height: rowHeight,
},
};
});
}, [
schedules,
startDate,
cellWidth,
resourceColumnWidth,
rowHeight,
totalCells,
]);
const handleGridClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (!onCellClick) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const cellIndex = Math.floor(x / cellWidth);
const clickedDate = new Date(startDate);
clickedDate.setDate(clickedDate.getDate() + cellIndex);
onCellClick(resource.id, clickedDate);
};
return (
<div
className="flex border-b hover:bg-muted/20"
style={{ height: rowHeight }}
>
{/* 리소스 컬럼 */}
<div
className="sticky left-0 z-10 flex shrink-0 items-center border-r bg-muted/30 px-2 sm:px-3"
style={{ width: resourceColumnWidth }}
>
<div className="truncate">
<div className="truncate text-[10px] font-medium sm:text-sm">
{resource.name}
</div>
{resource.group && (
<div className="truncate text-[9px] text-muted-foreground sm:text-xs">
{resource.group}
</div>
)}
</div>
</div>
{/* 타임라인 그리드 */}
<div
className="relative flex-1"
style={{ width: gridWidth }}
onClick={handleGridClick}
>
{/* 배경 그리드 */}
<div className="absolute inset-0 flex">
{Array.from({ length: totalCells }).map((_, idx) => {
const cellDate = new Date(startDate);
cellDate.setDate(cellDate.getDate() + idx);
const isWeekend =
cellDate.getDay() === 0 || cellDate.getDay() === 6;
const isToday = cellDate.getTime() === today.getTime();
const isMonthStart = cellDate.getDate() === 1;
return (
<div
key={idx}
className={cn(
"h-full border-r",
isWeekend && "bg-muted/20",
isToday && "bg-primary/5",
isMonthStart && "border-l-2 border-l-primary/20"
)}
style={{ width: cellWidth }}
/>
);
})}
</div>
{/* 스케줄 바들 */}
{schedulePositions.map(({ schedule, position, isMilestone }) => (
<ScheduleBar
key={schedule.id}
schedule={schedule}
position={{
...position,
left: position.left - resourceColumnWidth,
}}
config={config}
draggable={config.draggable}
resizable={config.resizable}
hasConflict={conflictIds?.has(schedule.id) ?? false}
isMilestone={isMilestone}
onClick={() => onScheduleClick?.(schedule)}
onDragComplete={onDragComplete}
onResizeComplete={onResizeComplete}
/>
))}
</div>
</div>
);
}