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

196 lines
5.1 KiB
TypeScript

"use client";
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import { DateCell, ZoomLevel } from "../types";
import { dayLabels, monthLabels } from "../config";
interface TimelineHeaderProps {
/** 시작 날짜 */
startDate: Date;
/** 종료 날짜 */
endDate: Date;
/** 줌 레벨 */
zoomLevel: ZoomLevel;
/** 셀 너비 */
cellWidth: number;
/** 헤더 높이 */
headerHeight: number;
/** 리소스 컬럼 너비 */
resourceColumnWidth: number;
/** 오늘 표시선 */
showTodayLine?: boolean;
}
/**
* 날짜 범위 내의 모든 날짜 셀 생성
*/
const generateDateCells = (
startDate: Date,
endDate: Date,
zoomLevel: ZoomLevel
): DateCell[] => {
const cells: DateCell[] = [];
const today = new Date();
today.setHours(0, 0, 0, 0);
const current = new Date(startDate);
current.setHours(0, 0, 0, 0);
while (current <= endDate) {
const date = new Date(current);
const dayOfWeek = date.getDay();
const isToday = date.getTime() === today.getTime();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const isMonthStart = date.getDate() === 1;
let label = "";
if (zoomLevel === "day") {
label = `${date.getDate()}(${dayLabels[dayOfWeek]})`;
} else if (zoomLevel === "week") {
// 주간: 월요일 기준 주 시작
if (dayOfWeek === 1 || cells.length === 0) {
label = `${date.getMonth() + 1}/${date.getDate()}`;
}
} else if (zoomLevel === "month") {
// 월간: 월 시작일만 표시
if (isMonthStart || cells.length === 0) {
label = monthLabels[date.getMonth()];
}
}
cells.push({
date,
label,
isToday,
isWeekend,
isMonthStart,
});
current.setDate(current.getDate() + 1);
}
return cells;
};
/**
* 월 헤더 그룹 생성 (상단 행)
*/
const generateMonthGroups = (
cells: DateCell[]
): { month: string; year: number; count: number }[] => {
const groups: { month: string; year: number; count: number }[] = [];
cells.forEach((cell) => {
const month = monthLabels[cell.date.getMonth()];
const year = cell.date.getFullYear();
if (
groups.length === 0 ||
groups[groups.length - 1].month !== month ||
groups[groups.length - 1].year !== year
) {
groups.push({ month, year, count: 1 });
} else {
groups[groups.length - 1].count++;
}
});
return groups;
};
export function TimelineHeader({
startDate,
endDate,
zoomLevel,
cellWidth,
headerHeight,
resourceColumnWidth,
showTodayLine = true,
}: TimelineHeaderProps) {
// 날짜 셀 생성
const dateCells = useMemo(
() => generateDateCells(startDate, endDate, zoomLevel),
[startDate, endDate, zoomLevel]
);
// 월 그룹 생성
const monthGroups = useMemo(() => generateMonthGroups(dateCells), [dateCells]);
// 오늘 위치 계산
const todayPosition = useMemo(() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayIndex = dateCells.findIndex(
(cell) => cell.date.getTime() === today.getTime()
);
if (todayIndex === -1) return null;
return resourceColumnWidth + todayIndex * cellWidth + cellWidth / 2;
}, [dateCells, cellWidth, resourceColumnWidth]);
return (
<div
className="sticky top-0 z-20 border-b bg-background"
style={{ height: headerHeight }}
>
{/* 상단 행: 월/년도 */}
<div className="flex" style={{ height: headerHeight / 2 }}>
{/* 리소스 컬럼 헤더 */}
<div
className="flex-shrink-0 border-r bg-muted/50 flex items-center justify-center font-medium text-sm"
style={{ width: resourceColumnWidth }}
>
</div>
{/* 월 그룹 */}
{monthGroups.map((group, idx) => (
<div
key={`${group.year}-${group.month}-${idx}`}
className="border-r flex items-center justify-center text-xs font-medium text-muted-foreground"
style={{ width: group.count * cellWidth }}
>
{group.year} {group.month}
</div>
))}
</div>
{/* 하단 행: 일자 */}
<div className="flex" style={{ height: headerHeight / 2 }}>
{/* 리소스 컬럼 (빈칸) */}
<div
className="flex-shrink-0 border-r bg-muted/50"
style={{ width: resourceColumnWidth }}
/>
{/* 날짜 셀 */}
{dateCells.map((cell, idx) => (
<div
key={idx}
className={cn(
"border-r flex items-center justify-center text-xs",
cell.isToday && "bg-primary/10 font-bold text-primary",
cell.isWeekend && !cell.isToday && "bg-muted/30 text-muted-foreground",
cell.isMonthStart && "border-l-2 border-l-primary/30"
)}
style={{ width: cellWidth }}
>
{cell.label}
</div>
))}
</div>
{/* 오늘 표시선 */}
{showTodayLine && todayPosition !== null && (
<div
className="absolute top-0 bottom-0 w-0.5 bg-primary z-30 pointer-events-none"
style={{ left: todayPosition }}
/>
)}
</div>
);
}