183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useRef } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { ScheduleItem, ScheduleBarPosition, TimelineSchedulerConfig } from "../types";
|
|
import { statusOptions } from "../config";
|
|
|
|
interface ScheduleBarProps {
|
|
/** 스케줄 항목 */
|
|
schedule: ScheduleItem;
|
|
/** 위치 정보 */
|
|
position: ScheduleBarPosition;
|
|
/** 설정 */
|
|
config: TimelineSchedulerConfig;
|
|
/** 드래그 가능 여부 */
|
|
draggable?: boolean;
|
|
/** 리사이즈 가능 여부 */
|
|
resizable?: boolean;
|
|
/** 클릭 이벤트 */
|
|
onClick?: (schedule: ScheduleItem) => void;
|
|
/** 드래그 시작 */
|
|
onDragStart?: (schedule: ScheduleItem, e: React.MouseEvent) => void;
|
|
/** 드래그 중 */
|
|
onDrag?: (deltaX: number, deltaY: number) => void;
|
|
/** 드래그 종료 */
|
|
onDragEnd?: () => void;
|
|
/** 리사이즈 시작 */
|
|
onResizeStart?: (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => void;
|
|
/** 리사이즈 중 */
|
|
onResize?: (deltaX: number, direction: "start" | "end") => void;
|
|
/** 리사이즈 종료 */
|
|
onResizeEnd?: () => void;
|
|
}
|
|
|
|
export function ScheduleBar({
|
|
schedule,
|
|
position,
|
|
config,
|
|
draggable = true,
|
|
resizable = true,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
onResizeStart,
|
|
onResizeEnd,
|
|
}: ScheduleBarProps) {
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
const barRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 상태에 따른 색상
|
|
const statusColor = schedule.color ||
|
|
config.statusColors?.[schedule.status] ||
|
|
statusOptions.find((s) => s.value === schedule.status)?.color ||
|
|
"#3b82f6";
|
|
|
|
// 진행률 바 너비
|
|
const progressWidth = config.showProgress && schedule.progress !== undefined
|
|
? `${schedule.progress}%`
|
|
: "0%";
|
|
|
|
// 드래그 시작 핸들러
|
|
const handleMouseDown = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (!draggable || isResizing) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDragging(true);
|
|
onDragStart?.(schedule, e);
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
// 드래그 중 로직은 부모에서 처리
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
onDragEnd?.();
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
},
|
|
[draggable, isResizing, schedule, onDragStart, onDragEnd]
|
|
);
|
|
|
|
// 리사이즈 시작 핸들러
|
|
const handleResizeStart = useCallback(
|
|
(direction: "start" | "end", e: React.MouseEvent) => {
|
|
if (!resizable) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsResizing(true);
|
|
onResizeStart?.(schedule, direction, e);
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
// 리사이즈 중 로직은 부모에서 처리
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsResizing(false);
|
|
onResizeEnd?.();
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
},
|
|
[resizable, schedule, onResizeStart, onResizeEnd]
|
|
);
|
|
|
|
// 클릭 핸들러
|
|
const handleClick = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (isDragging || isResizing) return;
|
|
e.stopPropagation();
|
|
onClick?.(schedule);
|
|
},
|
|
[isDragging, isResizing, onClick, schedule]
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={barRef}
|
|
className={cn(
|
|
"absolute rounded-md shadow-sm cursor-pointer transition-shadow",
|
|
"hover:shadow-md hover:z-10",
|
|
isDragging && "opacity-70 shadow-lg z-20",
|
|
isResizing && "z-20",
|
|
draggable && "cursor-grab",
|
|
isDragging && "cursor-grabbing"
|
|
)}
|
|
style={{
|
|
left: position.left,
|
|
top: position.top + 4,
|
|
width: position.width,
|
|
height: position.height - 8,
|
|
backgroundColor: statusColor,
|
|
}}
|
|
onClick={handleClick}
|
|
onMouseDown={handleMouseDown}
|
|
>
|
|
{/* 진행률 바 */}
|
|
{config.showProgress && schedule.progress !== undefined && (
|
|
<div
|
|
className="absolute inset-y-0 left-0 rounded-l-md opacity-30 bg-white"
|
|
style={{ width: progressWidth }}
|
|
/>
|
|
)}
|
|
|
|
{/* 제목 */}
|
|
<div className="relative z-10 px-2 py-1 text-xs text-white truncate font-medium">
|
|
{schedule.title}
|
|
</div>
|
|
|
|
{/* 진행률 텍스트 */}
|
|
{config.showProgress && schedule.progress !== undefined && (
|
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-white/80 font-medium">
|
|
{schedule.progress}%
|
|
</div>
|
|
)}
|
|
|
|
{/* 리사이즈 핸들 - 왼쪽 */}
|
|
{resizable && (
|
|
<div
|
|
className="absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-white/20 rounded-l-md"
|
|
onMouseDown={(e) => handleResizeStart("start", e)}
|
|
/>
|
|
)}
|
|
|
|
{/* 리사이즈 핸들 - 오른쪽 */}
|
|
{resizable && (
|
|
<div
|
|
className="absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-white/20 rounded-r-md"
|
|
onMouseDown={(e) => handleResizeStart("end", e)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|