'use client'; /** * 공정 등록/수정 폼 컴포넌트 (리디자인) * * 기획서 스크린샷 1 기준: * - 기본 정보: 공정명(자동생성), 공정형, 담당부서, 담당자, 생산일자, 상태 * - 품목 설정 정보: 품목 선택 팝업 연동 * - 단계 관리: 단계 등록/수정/삭제 (인라인) * * 제거된 섹션: 자동분류규칙, 작업정보, 설명 */ import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, GripVertical, Trash2 } from 'lucide-react'; import { ReorderButtons } from '@/components/molecules'; 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 { RuleModal } from './RuleModal'; import { toast } from 'sonner'; import type { Process, ClassificationRule, ProcessType, ProcessStep } from '@/types/process'; import { PROCESS_TYPE_OPTIONS, PROCESS_CATEGORY_OPTIONS } from '@/types/process'; import { createProcess, updateProcess, getDepartmentOptions, getProcessSteps, getDocumentTemplates, type DepartmentOption, type DocumentTemplateOption, } 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 [processCategory, setProcessCategory] = useState( initialData?.processCategory || '' ); const [useProductionDate, setUseProductionDate] = useState( initialData?.useProductionDate ?? false ); const [isActive, setIsActive] = useState( initialData ? initialData.status === '사용중' : true ); const [isLoading, setIsLoading] = useState(false); // 중간검사/작업일지 설정 (Process 레벨) const [needsInspection, setNeedsInspection] = useState( initialData?.needsInspection ?? false ); const [documentTemplateId, setDocumentTemplateId] = useState( initialData?.documentTemplateId ); const [needsWorkLog, setNeedsWorkLog] = useState( initialData?.needsWorkLog ?? false ); const [workLogTemplateId, setWorkLogTemplateId] = useState( initialData?.workLogTemplateId ); const [documentTemplates, setDocumentTemplates] = useState([]); // 품목 분류 규칙 (기존 로직 유지) 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 categoryOptions = useMemo(() => { const name = processName.trim(); for (const [key, options] of Object.entries(PROCESS_CATEGORY_OPTIONS)) { if (name.includes(key)) return options; } return [{ value: '없음', label: '없음' }]; }, [processName]); // 공정명 변경 시 구분 값 리셋 useEffect(() => { if (categoryOptions.length === 0) { setProcessCategory(''); } else if (processCategory && !categoryOptions.find(o => o.value === processCategory)) { setProcessCategory(''); } }, [categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps // 개별 품목 목록 추출 const individualItems = classificationRules .filter((r) => r.registrationType === 'individual') .flatMap((r) => r.items || []); const itemCount = individualItems.length; // 품목 삭제 (로컬 상태에서 제거) const handleRemoveItem = useCallback((itemId: string) => { setClassificationRules((prev) => prev.map((rule) => { if (rule.registrationType !== 'individual') return rule; const filtered = (rule.items || []).filter((item) => item.id !== itemId); const newCondition = filtered.map((item) => item.id).join(','); return { ...rule, items: filtered, conditionValue: newCondition }; }).filter((rule) => rule.registrationType !== 'individual' || (rule.items && rule.items.length > 0)) ); }, []); // 부서 목록 + 문서양식 목록 로드 useEffect(() => { const loadInitialData = async () => { setIsDepartmentsLoading(true); const [departments, templates] = await Promise.all([ getDepartmentOptions(), getDocumentTemplates(), ]); setDepartmentOptions(departments); if (templates.success && templates.data) { setDocumentTemplates(templates.data); } setIsDepartmentsLoading(false); }; loadInitialData(); }, []); 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]); // 이미 등록된 품목 ID 목록 (RuleModal에서 필터링용) const registeredItemIds = useMemo(() => { const ids = new Set(); classificationRules .filter((r) => r.registrationType === 'individual') .forEach((r) => { // API에서 로드된 items (r.items || []).forEach((item) => ids.add(item.id)); // conditionValue (새로 선택된 것 포함) r.conditionValue.split(',').filter(Boolean).forEach((id) => ids.add(id)); }); return ids; }, [classificationRules]); // 품목 규칙 추가/수정 (기존 individual 규칙과 병합하여 중복 방지) const handleSaveRule = useCallback( (ruleData: Omit) => { if (editingRule) { setClassificationRules((prev) => prev.map((r) => (r.id === editingRule.id ? { ...r, ...ruleData } : r)) ); } else if (ruleData.registrationType === 'individual') { // 새로 선택된 품목 ID const newItemIds = ruleData.conditionValue.split(',').filter(Boolean); setClassificationRules((prev) => { const existingIndividualRule = prev.find((r) => r.registrationType === 'individual'); if (existingIndividualRule) { // 기존 individual 규칙에 병합 (중복 제거) const existingIds = existingIndividualRule.conditionValue.split(',').filter(Boolean); const mergedIds = [...new Set([...existingIds, ...newItemIds])]; return prev.map((r) => r.id === existingIndividualRule.id ? { ...r, conditionValue: mergedIds.join(',') } : r ); } else { // 새 규칙 생성 return [...prev, { ...ruleData, id: `rule-${Date.now()}`, createdAt: new Date().toISOString(), }]; } }); } 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); }, []); // 화살표 버튼으로 순서 변경 (로컬 state만 업데이트) const handleMoveStep = useCallback((fromIndex: number, toIndex: number) => { setSteps((prev) => { const updated = [...prev]; const [moved] = updated.splice(fromIndex, 1); updated.splice(toIndex, 0, moved); return updated.map((step, i) => ({ ...step, order: i + 1 })); }); }, []); 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, processCategory: processCategory || undefined, department, documentTemplateId: needsInspection ? (documentTemplateId || undefined) : undefined, needsInspection, needsWorkLog, workLogTemplateId: needsWorkLog ? workLogTemplateId : undefined, 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( () => ( <>
{/* 기본 정보 */} 기본 정보 {/* Row 1: 공정번호(수정시) | 공정명 | 담당부서 | 담당자 */}
{isEdit && initialData?.processCode && (
)}
setProcessName(e.target.value)} placeholder="예: 스크린" />
setManager(e.target.value)} placeholder="담당자명" />
{/* Row 2: 구분 | 생산일자 | 상태 */}
{/* Row 3: 중간검사여부 | 중간검사양식 | 작업일지여부 | 작업일지양식 */}
{needsInspection && (
)}
{needsWorkLog && (
)}
{/* 품목 설정 정보 */}
품목 설정 정보 {itemCount}개

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

{individualItems.length > 0 && (
{individualItems.map((item) => (
{item.code}
))}
)}
{/* 단계 테이블 */}
단계 {!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={`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'}
)}
{/* 품목 선택 모달 */} ), [ processName, processType, processCategory, categoryOptions, department, manager, useProductionDate, isActive, needsInspection, documentTemplateId, needsWorkLog, workLogTemplateId, documentTemplates, classificationRules, steps, isStepsLoading, ruleModalOpen, editingRule, departmentOptions, isDepartmentsLoading, itemCount, individualItems, registeredItemIds, handleRemoveItem, dragIndex, dragOverIndex, handleSaveRule, handleModalClose, handleDeleteStep, handleAddStep, handleStepClick, handleDragStart, handleDragOver, handleDragEnd, handleDrop, handleMoveStep, isEdit, initialData?.id, ] ); const config = isEdit ? processEditConfig : processCreateConfig; return ( ); }