'use client'; /** * 공정 등록/수정 폼 컴포넌트 (리디자인) * * 기획서 스크린샷 1 기준: * - 기본 정보: 공정명(자동생성), 공정형, 담당부서, 담당자, 생산일자, 상태 * - 품목 설정 정보: 품목 선택 팝업 연동 * - 단계 관리: 단계 등록/수정/삭제 (인라인) * * 제거된 섹션: 자동분류규칙, 작업정보, 설명 */ import { useState, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, GripVertical, Trash2, Package } from 'lucide-react'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { processCreateConfig, processEditConfig } from './processConfig'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { RuleModal } from './RuleModal'; import { toast } from 'sonner'; import type { Process, ClassificationRule, ProcessType, ProcessStep } from '@/types/process'; import { PROCESS_TYPE_OPTIONS } from '@/types/process'; import { createProcess, updateProcess, getDepartmentOptions, getProcessSteps, type DepartmentOption, } from './actions'; interface ProcessFormProps { mode: 'create' | 'edit'; initialData?: Process; } export function ProcessForm({ mode, initialData }: ProcessFormProps) { const router = useRouter(); const isEdit = mode === 'edit'; // 기본 정보 상태 const [processName, setProcessName] = useState(initialData?.processName || ''); const [processType, setProcessType] = useState( initialData?.processType || '생산' ); const [department, setDepartment] = useState(initialData?.department || ''); const [manager, setManager] = useState(initialData?.manager || ''); const [useProductionDate, setUseProductionDate] = useState( initialData?.useProductionDate ?? false ); const [isActive, setIsActive] = useState( initialData ? initialData.status === '사용중' : true ); const [isLoading, setIsLoading] = useState(false); // 품목 분류 규칙 (기존 로직 유지) const [classificationRules, setClassificationRules] = useState( initialData?.classificationRules || [] ); // 단계 목록 상태 const [steps, setSteps] = useState([]); const [isStepsLoading, setIsStepsLoading] = useState(isEdit); // 부서 목록 상태 const [departmentOptions, setDepartmentOptions] = useState([]); const [isDepartmentsLoading, setIsDepartmentsLoading] = useState(true); // 품목 선택 모달 상태 const [ruleModalOpen, setRuleModalOpen] = useState(false); const [editingRule, setEditingRule] = useState(undefined); // 드래그 상태 const [dragIndex, setDragIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); // 품목 개수 계산 const itemCount = classificationRules .filter((r) => r.registrationType === 'individual') .reduce((sum, r) => sum + (r.items?.length || 0), 0); // 부서 목록 + 단계 목록 로드 useEffect(() => { const loadDepartments = async () => { setIsDepartmentsLoading(true); const departments = await getDepartmentOptions(); setDepartmentOptions(departments); setIsDepartmentsLoading(false); }; loadDepartments(); }, []); useEffect(() => { if (isEdit && initialData?.id) { const loadSteps = async () => { setIsStepsLoading(true); const result = await getProcessSteps(initialData.id); if (result.success && result.data) { setSteps(result.data); } setIsStepsLoading(false); }; loadSteps(); } }, [isEdit, initialData?.id]); // 품목 규칙 추가/수정 const handleSaveRule = useCallback( (ruleData: Omit) => { if (editingRule) { setClassificationRules((prev) => prev.map((r) => (r.id === editingRule.id ? { ...r, ...ruleData } : r)) ); } else { const newRule: ClassificationRule = { ...ruleData, id: `rule-${Date.now()}`, createdAt: new Date().toISOString(), }; setClassificationRules((prev) => [...prev, newRule]); } setEditingRule(undefined); }, [editingRule] ); const handleModalClose = useCallback((open: boolean) => { setRuleModalOpen(open); if (!open) { setEditingRule(undefined); } }, []); // 단계 삭제 const handleDeleteStep = useCallback((stepId: string) => { setSteps((prev) => prev.filter((s) => s.id !== stepId)); }, []); // 단계 상세 이동 const handleStepClick = (stepId: string) => { if (isEdit && initialData?.id) { router.push(`/ko/master-data/process-management/${initialData.id}/steps/${stepId}`); } }; // 단계 등록 이동 const handleAddStep = () => { if (isEdit && initialData?.id) { router.push(`/ko/master-data/process-management/${initialData.id}/steps/new`); } else { toast.info('공정을 먼저 등록한 후 단계를 추가할 수 있습니다.'); } }; // 드래그&드롭 const dragNodeRef = useRef(null); 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'; } dragNodeRef.current = null; setDragIndex(null); setDragOverIndex(null); }, []); const handleDrop = useCallback( (e: React.DragEvent, dropIndex: number) => { e.preventDefault(); if (dragIndex === null || dragIndex === dropIndex) { handleDragEnd(); return; } setSteps((prev) => { const updated = [...prev]; const [moved] = updated.splice(dragIndex, 1); updated.splice(dropIndex, 0, moved); return updated.map((step, i) => ({ ...step, order: i + 1 })); }); handleDragEnd(); }, [dragIndex, handleDragEnd] ); // 제출 const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { if (!processName.trim()) { toast.error('공정명을 입력해주세요.'); return { success: false, error: '공정명을 입력해주세요.' }; } if (!department) { toast.error('담당부서를 선택해주세요.'); return { success: false, error: '담당부서를 선택해주세요.' }; } const formData = { processName: processName.trim(), processType, department, classificationRules: classificationRules.map((rule) => ({ registrationType: rule.registrationType, ruleType: rule.ruleType, matchingType: rule.matchingType, conditionValue: rule.conditionValue, priority: rule.priority, description: rule.description, isActive: rule.isActive, })), requiredWorkers: 1, workSteps: '', isActive, }; setIsLoading(true); try { if (isEdit && initialData?.id) { const result = await updateProcess(initialData.id, formData); if (result.success) { toast.success('공정이 수정되었습니다.'); router.push('/ko/master-data/process-management'); return { success: true }; } else { toast.error(result.error || '수정에 실패했습니다.'); return { success: false, error: result.error }; } } else { const result = await createProcess(formData); if (result.success) { toast.success('공정이 등록되었습니다.'); router.push('/ko/master-data/process-management'); return { success: true }; } else { toast.error(result.error || '등록에 실패했습니다.'); return { success: false, error: result.error }; } } } catch { toast.error('처리 중 오류가 발생했습니다.'); return { success: false, error: '처리 중 오류가 발생했습니다.' }; } finally { setIsLoading(false); } }; const handleCancel = () => { router.back(); }; // ===== 폼 콘텐츠 렌더링 ===== const renderFormContent = useCallback( () => ( <>
{/* 기본 정보 */} 기본 정보
setProcessName(e.target.value)} placeholder="예: 스크린" />
setManager(e.target.value)} placeholder="담당자명" />
{useProductionDate ? '사용' : '미사용'}
{/* 품목 설정 정보 */}
품목 설정 정보

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

{itemCount}개
{/* 단계 테이블 */}
단계 {!isStepsLoading && ( 총 {steps.length}건 )}
{isStepsLoading ? (
로딩 중...
) : steps.length === 0 ? (
{isEdit ? '등록된 단계가 없습니다. [단계 등록] 버튼으로 추가해주세요.' : '공정을 먼저 등록한 후 단계를 추가할 수 있습니다.'}
) : (
{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()} > {index + 1} {step.stepCode} {step.stepName} {step.isRequired ? 'Y' : 'N'} {step.needsApproval ? 'Y' : 'N'} {step.needsInspection ? 'Y' : 'N'} {step.isActive ? 'Y' : 'N'}
)}
{/* 품목 선택 모달 */} ), [ processName, processType, department, manager, useProductionDate, isActive, classificationRules, steps, isStepsLoading, ruleModalOpen, editingRule, departmentOptions, isDepartmentsLoading, itemCount, dragIndex, dragOverIndex, handleSaveRule, handleModalClose, handleDeleteStep, handleAddStep, handleStepClick, handleDragStart, handleDragOver, handleDragEnd, handleDrop, isEdit, initialData?.id, ] ); const config = isEdit ? processEditConfig : processCreateConfig; return ( ); }