'use client'; /** * 작업지시 등록 페이지 * API 연동 완료 (2025-12-26) * IntegratedDetailTemplate 마이그레이션 (2025-01-20) */ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft, FileText, X, Edit2, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { SalesOrderSelectModal } from './SalesOrderSelectModal'; import { AssigneeSelectModal } from './AssigneeSelectModal'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { createWorkOrder, getProcessOptions, type ProcessOption } from './actions'; import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types'; import { workOrderCreateConfig } from './workOrderConfig'; import { useDevFill } from '@/components/dev'; import { generateWorkOrderData } from '@/components/dev/generators/workOrderData'; // Validation 에러 타입 interface ValidationErrors { [key: string]: string; } // 필드명 매핑 const FIELD_NAME_MAP: Record = { selectedOrder: '수주', client: '발주처', projectName: '현장명', processId: '공정', shipmentDate: '출고예정일', }; type RegistrationMode = 'linked' | 'manual'; interface FormData { // 수주 정보 selectedOrder: SalesOrder | null; splitOption: 'all' | 'partial'; // 기본 정보 client: string; projectName: string; orderNo: string; itemCount: number; // 작업지시 정보 processId: number | null; // 공정 ID (FK → processes.id) shipmentDate: string; priority: number; assignees: string[]; // 비고 note: string; } const initialFormData: FormData = { selectedOrder: null, splitOption: 'all', client: '', projectName: '', orderNo: '', itemCount: 0, processId: null, shipmentDate: '', priority: 5, assignees: [], note: '', }; export function WorkOrderCreate() { const router = useRouter(); const [mode, setMode] = useState('linked'); const [formData, setFormData] = useState(initialFormData); const [isModalOpen, setIsModalOpen] = useState(false); const [isAssigneeModalOpen, setIsAssigneeModalOpen] = useState(false); const [assigneeNames, setAssigneeNames] = useState([]); const [validationErrors, setValidationErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [processOptions, setProcessOptions] = useState([]); const [isLoadingProcesses, setIsLoadingProcesses] = useState(true); // 공정 옵션 로드 useEffect(() => { async function loadProcessOptions() { setIsLoadingProcesses(true); const result = await getProcessOptions(); if (result.success) { setProcessOptions(result.data); // 첫 번째 공정을 기본값으로 설정 if (result.data.length > 0 && !formData.processId) { setFormData(prev => ({ ...prev, processId: result.data[0].id })); } } else { toast.error(result.error || '공정 목록을 불러오는데 실패했습니다.'); } setIsLoadingProcesses(false); } loadProcessOptions(); }, []); // DevToolbar 자동 채우기 useDevFill( 'workOrder', useCallback(async () => { // 공정 옵션 직접 가져오기 (state가 아직 로딩 전일 수 있음) const processResult = await getProcessOptions(); const processes = processResult.success ? processResult.data : []; const sampleData = generateWorkOrderData({ processOptions: processes }); // 수동 등록 모드로 변경 setMode('manual'); // 폼 데이터 채우기 setFormData(prev => ({ ...prev, client: '테스트 거래처', projectName: '테스트 현장', orderNo: '', itemCount: 0, processId: sampleData.processId, shipmentDate: sampleData.shipmentDate, priority: sampleData.priority, note: sampleData.note, })); toast.success('[Dev] 작업지시 폼이 자동으로 채워졌습니다.'); }, []) ); // 수주 선택 핸들러 const handleSelectOrder = (order: SalesOrder) => { setFormData({ ...formData, selectedOrder: order, client: order.client, projectName: order.projectName, orderNo: order.orderNo, itemCount: order.itemCount, }); }; // 수주 해제 const handleClearOrder = () => { setFormData({ ...initialFormData, processId: formData.processId, shipmentDate: formData.shipmentDate, priority: formData.priority, }); }; // 폼 제출 const handleSubmit = async () => { // Validation 체크 const errors: ValidationErrors = {}; if (mode === 'linked') { if (!formData.selectedOrder) { errors.selectedOrder = '수주를 선택해주세요'; } } else { if (!formData.client) { errors.client = '발주처를 입력해주세요'; } if (!formData.projectName) { errors.projectName = '현장명을 입력해주세요'; } } if (!formData.processId) { errors.processId = '공정을 선택해주세요'; } if (!formData.shipmentDate) { errors.shipmentDate = '출고예정일을 선택해주세요'; } // 에러가 있으면 상태 업데이트 후 리턴 if (Object.keys(errors).length > 0) { setValidationErrors(errors); // 페이지 상단으로 스크롤 window.scrollTo({ top: 0, behavior: 'smooth' }); return; } // 에러 초기화 setValidationErrors({}); setIsSubmitting(true); try { // API 호출 const result = await createWorkOrder({ salesOrderId: formData.selectedOrder?.id ? parseInt(formData.selectedOrder.id) : undefined, projectName: formData.projectName, processId: formData.processId!, // 공정 ID (FK → processes.id) scheduledDate: formData.shipmentDate, assigneeIds: formData.assignees.map(id => parseInt(id)), note: formData.note || undefined, }); if (result.success) { toast.success('작업지시가 등록되었습니다.'); router.push('/production/work-orders'); } else { toast.error(result.error || '작업지시 등록에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkOrderCreate] handleSubmit error:', error); toast.error('작업지시 등록 중 오류가 발생했습니다.'); } finally { setIsSubmitting(false); } }; // 취소 const handleCancel = () => { router.back(); }; // 선택된 공정의 코드 가져오기 const getSelectedProcessCode = (): string => { const selectedProcess = processOptions.find(p => p.id === formData.processId); return selectedProcess?.processCode || '-'; }; // 폼 컨텐츠 렌더링 const renderFormContent = useCallback(() => (
{/* Validation 에러 표시 */} {Object.keys(validationErrors).length > 0 && (
⚠️
입력 내용을 확인해주세요 ({Object.keys(validationErrors).length}개 오류)
    {Object.entries(validationErrors).map(([field, message]) => { const fieldName = FIELD_NAME_MAP[field] || field; return (
  • {fieldName}: {message}
  • ); })}
)} {/* 등록 방식 */}

등록 방식

setMode(value as RegistrationMode)} className="flex gap-6" >
{/* 수주 정보 (연동 모드) */} {mode === 'linked' && (

수주 정보

{!formData.selectedOrder ? (

수주에서 불러오기

회계확인 완료된 수주를 선택하면 정보가 자동으로 채워집니다

) : (
{formData.selectedOrder.orderNo} {formData.selectedOrder.status}에서 불러옴

{formData.selectedOrder.client} / {formData.selectedOrder.projectName} / {formData.selectedOrder.itemCount}개 품목

{/* 분할 선택 */}

분할 선택

setFormData({ ...formData, splitOption: value as 'all' | 'partial' }) } className="flex gap-4" >
)}
)} {/* 기본 정보 */}

기본 정보

setFormData({ ...formData, client: e.target.value })} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '발주처 입력'} disabled={mode === 'linked'} className="bg-white" />
setFormData({ ...formData, projectName: e.target.value })} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '현장명 입력'} disabled={mode === 'linked'} className="bg-white" />
setFormData({ ...formData, orderNo: e.target.value })} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '수주번호 입력'} disabled={mode === 'linked'} className="bg-white" />
setFormData({ ...formData, itemCount: parseInt(e.target.value) || 0 })} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '품목수 입력'} disabled={mode === 'linked'} className="bg-white" />
{/* 작업지시 정보 */}

작업지시 정보

공정코드: {getSelectedProcessCode()}

setFormData({ ...formData, shipmentDate: date })} />
setIsAssigneeModalOpen(true)} className="flex min-h-10 w-full cursor-pointer items-center rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background hover:bg-accent/50" > {assigneeNames.length > 0 ? ( {assigneeNames.join(', ')} ) : ( 담당자를 선택하세요 (팀/개인) )}
{/* 비고 */}

비고