196 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|