'use client'; /** * 작업지시 등록 페이지 * API 연동 완료 (2025-12-26) * IntegratedDetailTemplate 마이그레이션 (2025-01-20) */ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } 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 { 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, searchItemsForWorkOrder, type ProcessOption, type ManualItemOption } from './actions'; import { type SalesOrder } from './types'; import { workOrderCreateConfig } from './workOrderConfig'; import { useDevFill } from '@/components/dev'; import { generateWorkOrderData } from '@/components/dev/generators/workOrderData'; // 수동 등록 품목 interface ManualItem { item_id?: number; item_name: string; specification: string; quantity: number; unit: string; } 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); // 수동 등록 품목 관리 const [manualItems, setManualItems] = useState([]); const [itemSearchQuery, setItemSearchQuery] = useState(''); const [itemSearchResults, setItemSearchResults] = useState([]); const [isSearchingItems, setIsSearchingItems] = useState(false); const [showItemSearch, setShowItemSearch] = useState(false); // 필드 에러 클리어 헬퍼 const clearFieldError = useCallback((field: string) => { if (validationErrors[field]) { setValidationErrors(prev => { const next = { ...prev }; delete next[field]; return next; }); } }, [validationErrors]); // 공정 옵션 로드 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, }); clearFieldError('selectedOrder'); }; // 수주 해제 const handleClearOrder = () => { setFormData({ ...initialFormData, processId: formData.processId, shipmentDate: formData.shipmentDate, priority: formData.priority, }); }; // 품목 검색 (수동 등록용) const handleItemSearch = async (query: string) => { setItemSearchQuery(query); if (query.length < 1) { setItemSearchResults([]); return; } setIsSearchingItems(true); const result = await searchItemsForWorkOrder(query, 'BENDING'); if (result.success) { setItemSearchResults(result.data); } setIsSearchingItems(false); }; // 검색 결과에서 품목 추가 const handleAddItem = (item: ManualItemOption) => { // 이미 추가된 품목인지 확인 if (manualItems.some(mi => mi.item_id === item.id)) { toast.error('이미 추가된 품목입니다.'); return; } setManualItems(prev => [...prev, { item_id: item.id, item_name: item.name, specification: item.specification, quantity: 1, unit: item.unit, }]); setShowItemSearch(false); setItemSearchQuery(''); setItemSearchResults([]); clearFieldError('items'); }; // 품목 수량 변경 const handleItemQtyChange = (index: number, qty: number) => { setManualItems(prev => prev.map((item, i) => i === index ? { ...item, quantity: qty } : item)); }; // 품목 삭제 const handleRemoveItem = (index: number) => { setManualItems(prev => prev.filter((_, i) => i !== index)); }; // 폼 제출 const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { // Validation 체크 const errors: Record = {}; if (mode === 'linked') { if (!formData.selectedOrder) { errors.selectedOrder = '수주를 선택해주세요'; } } else { if (!formData.client) { errors.client = '발주처를 입력해주세요'; } if (!formData.projectName) { errors.projectName = '현장명을 입력해주세요'; } if (manualItems.length === 0) { errors.items = '품목을 1개 이상 추가해주세요'; } } if (!formData.processId) { errors.processId = '공정을 선택해주세요'; } if (!formData.shipmentDate) { errors.shipmentDate = '출고예정일을 선택해주세요'; } // 에러가 있으면 상태 업데이트 후 리턴 if (Object.keys(errors).length > 0) { setValidationErrors(errors); const firstError = Object.values(errors)[0]; toast.error(firstError); return { success: false, error: '' }; } // 에러 초기화 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, ...(mode === 'manual' && manualItems.length > 0 ? { items: manualItems.map(item => ({ item_id: item.item_id, item_name: item.item_name, specification: item.specification, quantity: item.quantity, unit: item.unit, })), } : {}), }); if (!result.success) { return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' }; } return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkOrderCreate] handleSubmit error:', error); const errorMessage = error instanceof Error ? error.message : '작업지시 등록 중 오류가 발생했습니다.'; return { success: false, error: errorMessage }; } 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(() => (
{/* 등록 방식 */}

등록 방식

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" >
)} {validationErrors.selectedOrder &&

{validationErrors.selectedOrder}

}
)} {/* 기본 정보 */}

기본 정보

{ setFormData({ ...formData, client: e.target.value }); clearFieldError('client'); }} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '발주처 입력'} disabled={mode === 'linked'} className={`bg-white ${validationErrors.client ? 'border-red-500' : ''}`} /> {validationErrors.client &&

{validationErrors.client}

}
{ setFormData({ ...formData, projectName: e.target.value }); clearFieldError('projectName'); }} placeholder={mode === 'linked' ? '수주를 선택하면 자동 입력됩니다' : '현장명 입력'} disabled={mode === 'linked'} className={`bg-white ${validationErrors.projectName ? 'border-red-500' : ''}`} /> {validationErrors.projectName &&

{validationErrors.projectName}

}
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" />
{/* 작업지시 정보 */}

작업지시 정보

{validationErrors.processId &&

{validationErrors.processId}

}

공정코드: {getSelectedProcessCode()}

{ setFormData({ ...formData, shipmentDate: date }); clearFieldError('shipmentDate'); }} className={validationErrors.shipmentDate ? 'border-red-500' : ''} /> {validationErrors.shipmentDate &&

{validationErrors.shipmentDate}

}
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(', ')} ) : ( 담당자를 선택하세요 (팀/개인) )}
{/* 품목 (수동 등록 모드) */} {mode === 'manual' && (

품목 목록 {manualItems.length > 0 && ( ({manualItems.length}개) )}

{/* 품목 검색 */} {showItemSearch && (
handleItemSearch(e.target.value)} placeholder="절곡품 코드 또는 품목명으로 검색" className="pl-9" />
{isSearchingItems && (
검색 중...
)} {itemSearchResults.length > 0 && (
{itemSearchResults.map((item) => ( ))}
)} {itemSearchQuery.length > 0 && !isSearchingItems && itemSearchResults.length === 0 && (

검색 결과가 없습니다.

)}
)} {/* 품목 목록 테이블 */} {manualItems.length > 0 ? (
{manualItems.map((item, index) => ( ))}
No 품목명 규격 수량 단위
{index + 1} {item.item_name} {item.specification || '-'} handleItemQtyChange(index, parseInt(e.target.value) || 1)} className="w-24 text-center mx-auto h-8" /> {item.unit}
) : (

추가된 품목이 없습니다.

위의 '품목 추가' 버튼으로 절곡품을 검색하여 추가하세요.

)} {validationErrors.items && (

{validationErrors.items}

)}
)} {/* 비고 */}

비고