'use client'; /** * 공정 상세 페이지 (리디자인) * * 기획서 스크린샷 1 기준: * - 기본 정보: 공정번호, 공정형, 담당부서, 담당자, 생산일자, 상태 * - 품목 설정 정보: 품목 선택 버튼 + 개수 표시 * - 단계 테이블: 드래그&드롭 순서변경 + 단계 등록 버튼 */ import { useState, useEffect, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft, Copy, Edit, GripVertical, Plus, Trash2 } from 'lucide-react'; import { ReorderButtons } from '@/components/molecules'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { useMenuStore } from '@/stores/menuStore'; import { usePermission } from '@/hooks/usePermission'; import { toast } from 'sonner'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { getProcessSteps, reorderProcessSteps, removeProcessItem, deleteProcess, duplicateProcess } from './actions'; import type { Process, ProcessStep } from '@/types/process'; interface ProcessDetailProps { process: Process; onProcessUpdate?: (process: Process) => void; } export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps) { const router = useRouter(); const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); const { canUpdate } = usePermission(); // 삭제 다이얼로그 const deleteDialog = useDeleteDialog({ onDelete: deleteProcess, onSuccess: () => router.push('/ko/master-data/process-management'), entityName: '공정', }); // 단계 목록 상태 const [steps, setSteps] = useState([]); const [isStepsLoading, setIsStepsLoading] = useState(true); // 드래그 상태 const [dragIndex, setDragIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const dragNodeRef = useRef(null); // 개별 품목 목록 추출 const individualItems = process.classificationRules .filter((r) => r.registrationType === 'individual') .flatMap((r) => r.items || []); const itemCount = individualItems.length; // 단계 목록 로드 useEffect(() => { const loadSteps = async () => { setIsStepsLoading(true); const result = await getProcessSteps(process.id); if (result.success && result.data) { setSteps(result.data); } setIsStepsLoading(false); }; loadSteps(); }, [process.id]); // 품목 삭제 const handleRemoveItem = async (itemId: string) => { const remainingIds = individualItems .filter((item) => item.id !== itemId) .map((item) => parseInt(item.id, 10)); const result = await removeProcessItem(process.id, remainingIds); if (result.success && result.data) { toast.success('품목이 제거되었습니다.'); onProcessUpdate?.(result.data); } else { toast.error(result.error || '품목 제거에 실패했습니다.'); } }; // 품목 전체 삭제 const [showRemoveAllDialog, setShowRemoveAllDialog] = useState(false); const handleRemoveAllItems = async () => { const result = await removeProcessItem(process.id, []); if (result.success && result.data) { toast.success('품목이 모두 제거되었습니다.'); onProcessUpdate?.(result.data); } else { toast.error(result.error || '품목 전체 제거에 실패했습니다.'); } setShowRemoveAllDialog(false); }; const [isDuplicating, setIsDuplicating] = useState(false); // 공정 복제 const handleDuplicate = async () => { setIsDuplicating(true); const result = await duplicateProcess(process.id); setIsDuplicating(false); if (result.success && result.data) { toast.success('공정이 복제되었습니다.'); router.push(`/ko/master-data/process-management/${result.data.id}?mode=view`); } else { toast.error(result.error || '공정 복제에 실패했습니다.'); } }; // 네비게이션 const handleEdit = () => { router.push(`/ko/master-data/process-management/${process.id}?mode=edit`); }; const handleList = () => { router.push('/ko/master-data/process-management'); }; const handleAddStep = () => { router.push(`/ko/master-data/process-management/${process.id}/steps/new?mode=new`); }; const handleStepClick = (stepId: string) => { router.push(`/ko/master-data/process-management/${process.id}/steps/${stepId}?mode=view`); }; // ===== 드래그&드롭 (HTML5 네이티브) ===== const handleDragStart = useCallback((e: React.DragEvent, index: number) => { setDragIndex(index); dragNodeRef.current = e.currentTarget; e.dataTransfer.effectAllowed = 'move'; // 약간의 딜레이로 드래그 시작 시 스타일 적용 requestAnimationFrame(() => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '0.4'; } }); }, []); const handleDragOver = useCallback((e: React.DragEvent, index: number) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverIndex(index); }, []); const handleDragEnd = useCallback(() => { if (dragNodeRef.current) { dragNodeRef.current.style.opacity = '1'; } setDragIndex(null); setDragOverIndex(null); dragNodeRef.current = null; }, []); const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => { e.preventDefault(); if (dragIndex === null || dragIndex === dropIndex) return; setSteps((prev) => { const updated = [...prev]; const [moved] = updated.splice(dragIndex, 1); updated.splice(dropIndex, 0, moved); const reordered = updated.map((step, i) => ({ ...step, order: i + 1 })); // API 순서 변경 호출 reorderProcessSteps( process.id, reordered.map((s) => ({ id: s.id, order: s.order })) ); return reordered; }); handleDragEnd(); }, [dragIndex, handleDragEnd, process.id]); // 화살표 버튼으로 순서 변경 const handleMoveStep = useCallback((fromIndex: number, toIndex: number) => { setSteps((prev) => { const updated = [...prev]; const [moved] = updated.splice(fromIndex, 1); updated.splice(toIndex, 0, moved); const reordered = updated.map((step, i) => ({ ...step, order: i + 1 })); reorderProcessSteps( process.id, reordered.map((s) => ({ id: s.id, order: s.order })) ); return reordered; }); }, [process.id]); return ( {/* 헤더 */}
{/* 기본 정보 */} 기본 정보 {/* Row 1: 공정번호 | 공정명 | 담당부서 | 담당자 */}
공정번호
{process.processCode}
공정명
{process.processName}
담당부서
{process.department || '-'}
담당자
{process.manager || '-'}
{/* Row 2: 구분 | 생산일자 | 상태 */}
구분
{process.processCategory || '없음'}
생산일자
{process.useProductionDate ? '사용' : '미사용'}
상태
{process.status}
{/* Row 3: 중간검사여부 | 중간검사양식 | 작업일지여부 | 작업일지양식 */}
중간검사 여부
{process.needsInspection ? '사용' : '미사용'}
{process.needsInspection && (
중간검사 양식
{process.documentTemplateName || '-'}
)}
작업일지 여부
{process.needsWorkLog ? '사용' : '미사용'}
{process.needsWorkLog && (
작업일지 양식
{process.workLogTemplateName || '-'}
)}
{/* 품목 설정 정보 */}
품목 설정 정보 {itemCount}개 {itemCount > 0 && ( )}

품목을 선택하면 이 공정으로 분류됩니다

{individualItems.length > 0 && (
{individualItems.map((item) => (
{item.code}
))}
)}
{/* 단계 테이블 */}
단계 {!isStepsLoading && ( 총 {steps.length}건 )}
{isStepsLoading ? (
로딩 중...
) : steps.length === 0 ? (
등록된 단계가 없습니다. [단계 등록] 버튼으로 추가해주세요.
) : ( <> {/* 모바일: 카드 리스트 */}
{steps.map((step, index) => (
handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDragEnd={handleDragEnd} onDrop={(e) => handleDrop(e, index)} onClick={() => handleStepClick(step.id)} className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer hover:bg-muted/50 ${ dragOverIndex === index && dragIndex !== index ? 'border-t-2 border-t-primary' : '' }`} > handleMoveStep(index, index - 1)} onMoveDown={() => handleMoveStep(index, index + 1)} isFirst={index === 0} isLast={index === steps.length - 1} size="xs" /> {index + 1}
{step.stepCode} {step.stepName}
필수 승인 검사
))}
{/* 데스크탑: 테이블 */}
{steps.map((step, index) => ( handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDragEnd={handleDragEnd} onDrop={(e) => handleDrop(e, index)} onClick={() => handleStepClick(step.id)} className={`border-b cursor-pointer transition-colors hover:bg-muted/50 ${ dragOverIndex === index && dragIndex !== index ? 'border-t-2 border-t-primary' : '' }`} > ))}
No. 단계코드 단계명 필수여부 승인여부 검사여부 사용
e.stopPropagation()}>
handleMoveStep(index, index - 1)} onMoveDown={() => handleMoveStep(index, index + 1)} isFirst={index === 0} isLast={index === steps.length - 1} size="xs" />
{index + 1} {step.stepCode} {step.stepName} {step.isRequired ? 'Y' : 'N'} {step.needsApproval ? 'Y' : 'N'} {step.needsInspection ? 'Y' : 'N'} {step.isActive ? 'Y' : 'N'}
)}
{/* 하단 액션 버튼 (sticky) */}
{canUpdate && (
)}
{/* 삭제 확인 다이얼로그 */} {/* 품목 전체 삭제 확인 다이얼로그 */}
); }