- Modified the production controller to replace the generic Request type with AuthenticatedRequest for better type safety and to ensure user authentication is handled correctly. - This change enhances the security and clarity of the API endpoints related to production plan management, ensuring that user-specific data is accessed appropriately. Made-with: Cursor
183 lines
5.5 KiB
TypeScript
183 lines
5.5 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 cursor-pointer rounded-md shadow-sm transition-shadow",
|
|
"hover:z-10 hover:shadow-md",
|
|
isDragging && "z-20 opacity-70 shadow-lg",
|
|
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 bg-white opacity-30"
|
|
style={{ width: progressWidth }}
|
|
/>
|
|
)}
|
|
|
|
{/* 제목 */}
|
|
<div className="relative z-10 truncate px-1.5 py-0.5 text-[10px] font-medium text-white sm:px-2 sm:py-1 sm:text-xs">
|
|
{schedule.title}
|
|
</div>
|
|
|
|
{/* 진행률 텍스트 */}
|
|
{config.showProgress && schedule.progress !== undefined && (
|
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 text-[8px] font-medium text-white/80 sm:right-2 sm:text-[10px]">
|
|
{schedule.progress}%
|
|
</div>
|
|
)}
|
|
|
|
{/* 리사이즈 핸들 - 왼쪽 */}
|
|
{resizable && (
|
|
<div
|
|
className="absolute bottom-0 left-0 top-0 w-1.5 cursor-ew-resize rounded-l-md hover:bg-white/20 sm:w-2"
|
|
onMouseDown={(e) => handleResizeStart("start", e)}
|
|
/>
|
|
)}
|
|
|
|
{/* 리사이즈 핸들 - 오른쪽 */}
|
|
{resizable && (
|
|
<div
|
|
className="absolute bottom-0 right-0 top-0 w-1.5 cursor-ew-resize rounded-r-md hover:bg-white/20 sm:w-2"
|
|
onMouseDown={(e) => handleResizeStart("end", e)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|