'use client'; /** * 입고 상세/등록/수정 페이지 * 기획서 2026-01-28 기준 마이그레이션 * * mode 패턴: * - view: 상세 조회 (읽기 전용) * - edit: 수정 모드 * - new: 신규 등록 모드 * * 섹션: * 1. 기본 정보 - 로트번호, 품목코드, 품목명, 규격, 단위, 발주처, 입고수량, 입고일, 작성자, 상태, 비고 * 2. 수입검사 정보 - 검사일, 검사결과, 업체 제공 성적서 자료 */ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Upload, FileText, Search, X, Plus, ClipboardCheck } from 'lucide-react'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { ItemSearchModal } from '@/components/quotes/ItemSearchModal'; import { InspectionModalV2 } from '@/app/[locale]/(protected)/quality/qms/components/InspectionModalV2'; import { ImportInspectionInputModal } from './ImportInspectionInputModal'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; // import { SupplierSearchModal } from './SupplierSearchModal'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { receivingConfig } from './receivingConfig'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; import { getReceivingById, createReceiving, updateReceiving, checkInspectionTemplate, } from './actions'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { RECEIVING_STATUS_OPTIONS, type ReceivingDetail as ReceivingDetailType, type ReceivingStatus, type InventoryAdjustmentRecord, } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { toast } from 'sonner'; import { useDevFill, generateReceivingData } from '@/components/dev'; interface Props { id: string; mode?: 'view' | 'edit' | 'new'; } // 초기 폼 데이터 const INITIAL_FORM_DATA: Partial = { materialNo: '', supplierMaterialNo: '', lotNo: '', itemCode: '', itemName: '', specification: '', unit: 'EA', supplier: '', manufacturer: '', receivingQty: undefined, receivingDate: '', createdBy: '', status: 'receiving_pending', remark: '', inspectionDate: '', inspectionResult: '', certificateFile: undefined, inventoryAdjustments: [], }; // 로트번호 생성 (YYMMDD-NN) function generateLotNo(): string { const now = new Date(); const yy = String(now.getFullYear()).slice(-2); const mm = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const seq = String(Math.floor(Math.random() * 100)).padStart(2, '0'); return `${yy}${mm}${dd}-${seq}`; } // localStorage에서 로그인 사용자 정보 가져오기 function getLoggedInUser(): { name: string; department: string } { if (typeof window === 'undefined') return { name: '', department: '' }; try { const userData = localStorage.getItem('user'); if (userData) { const parsed = JSON.parse(userData); return { name: parsed.name || '', department: parsed.department || '' }; } } catch { // ignore } return { name: '', department: '' }; } function getLoggedInUserName(): string { return getLoggedInUser().name; } export function ReceivingDetail({ id, mode = 'view' }: Props) { const router = useRouter(); const isNewMode = mode === 'new' || id === 'new'; const isEditMode = mode === 'edit'; const isViewMode = mode === 'view' && !isNewMode; // API 데이터 상태 const [detail, setDetail] = useState(null); const [isLoading, setIsLoading] = useState(!isNewMode); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); // 폼 데이터 (등록/수정 모드용) const [formData, setFormData] = useState>(INITIAL_FORM_DATA); // 업로드된 파일 상태 (File 객체) const [uploadedFile, setUploadedFile] = useState(null); // 수입검사 성적서 모달 상태 const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); // 수입검사 입력 모달 상태 const [isImportInspectionModalOpen, setIsImportInspectionModalOpen] = useState(false); const [isItemSearchOpen, setIsItemSearchOpen] = useState(false); const [isSupplierSearchOpen, setIsSupplierSearchOpen] = useState(false); // 수입검사 성적서 템플릿 존재 여부 const [hasInspectionTemplate, setHasInspectionTemplate] = useState(false); // 수입검사 첨부파일 (document_attachments) const [inspectionAttachments, setInspectionAttachments] = useState>([]); // 재고 조정 이력 상태 const [adjustments, setAdjustments] = useState([]); // Dev 모드 폼 자동 채우기 useDevFill( 'receiving', useCallback(async () => { if (!isNewMode) return; const data = generateReceivingData(); setFormData((prev) => ({ ...prev, lotNo: generateLotNo(), itemCode: data.itemCode, itemName: data.itemName, specification: data.specification, unit: data.unit, supplier: data.supplier, receivingQty: data.receivingQty, receivingDate: data.receivingDate, createdBy: getLoggedInUserName(), status: data.status as ReceivingStatus, remark: data.remark, })); }, [isNewMode]) ); // API 데이터 로드 const loadData = useCallback(async () => { if (isNewMode) { setIsLoading(false); return; } setIsLoading(true); setError(null); try { const result = await getReceivingById(id); if (result.success && result.data) { setDetail(result.data); // 재고 조정 이력 설정 if (result.data.inventoryAdjustments) { setAdjustments(result.data.inventoryAdjustments); } // 수정 모드일 때 폼 데이터 설정 if (isEditMode) { setFormData({ materialNo: result.data.materialNo || '', supplierMaterialNo: result.data.supplierMaterialNo || '', lotNo: result.data.lotNo || '', itemCode: result.data.itemCode, itemName: result.data.itemName, specification: result.data.specification || '', unit: result.data.unit || 'EA', supplier: result.data.supplier, manufacturer: result.data.manufacturer || '', receivingQty: result.data.receivingQty, receivingDate: result.data.receivingDate || '', createdBy: result.data.createdBy || '', status: result.data.status, remark: result.data.remark || '', inspectionDate: result.data.inspectionDate || '', inspectionResult: result.data.inspectionResult || '', certificateFile: result.data.certificateFile, }); } // 수입검사 성적서 템플릿 존재 여부 + 첨부파일 확인 if (result.data.itemId) { const templateCheck = await checkInspectionTemplate(result.data.itemId); setHasInspectionTemplate(templateCheck.hasTemplate); if (templateCheck.attachments && templateCheck.attachments.length > 0) { setInspectionAttachments(templateCheck.attachments); } } else { setHasInspectionTemplate(false); } } else { setError(result.error || '입고 정보를 찾을 수 없습니다.'); } } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ReceivingDetail] loadData error:', err); setError('데이터를 불러오는 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [id, isNewMode, isEditMode]); // 데이터 로드 useEffect(() => { loadData(); }, [loadData]); // 폼 입력 핸들러 const handleInputChange = (field: keyof ReceivingDetailType, value: string | number | undefined) => { setFormData((prev) => ({ ...prev, [field]: value, })); }; // 저장 핸들러 - 결과 반환 const handleSave = async (): Promise<{ success: boolean; error?: string }> => { setIsSaving(true); try { if (isNewMode) { const result = await createReceiving(formData); if (result.success) { toast.success('입고가 등록되었습니다.'); router.push('/ko/material/receiving-management'); return { success: true }; } else { toast.error(result.error || '등록에 실패했습니다.'); return { success: false, error: result.error }; } } else if (isEditMode) { const result = await updateReceiving(id, formData); if (result.success) { toast.success('입고 정보가 수정되었습니다.'); router.push(`/ko/material/receiving-management/${id}?mode=view`); return { success: true }; } else { toast.error(result.error || '수정에 실패했습니다.'); return { success: false, error: result.error }; } } return { success: false, error: '알 수 없는 모드입니다.' }; } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ReceivingDetail] handleSave error:', err); toast.error('저장 중 오류가 발생했습니다.'); return { success: false, error: '저장 중 오류가 발생했습니다.' }; } finally { setIsSaving(false); } }; // 수입검사하기 버튼 핸들러 - 수입검사 입력 모달 표시 const handleInspection = () => { setIsImportInspectionModalOpen(true); }; // 수입검사성적서 보기 버튼 핸들러 - 성적서 모달 표시 const handleViewInspectionReport = () => { setIsInspectionModalOpen(true); }; // 수입검사 저장 완료 핸들러 → 데이터 새로고침 const handleImportInspectionSave = () => { loadData(); }; // 재고 조정 행 추가 const handleAddAdjustment = () => { const newRecord: InventoryAdjustmentRecord = { id: `adj-${Date.now()}`, adjustmentDate: new Date().toISOString().split('T')[0], quantity: 0, inspector: getLoggedInUserName() || '홍길동', }; setAdjustments((prev) => [...prev, newRecord]); }; // 재고 조정 행 삭제 const handleRemoveAdjustment = (adjId: string) => { setAdjustments((prev) => prev.filter((a) => a.id !== adjId)); }; // 재고 조정 수량 변경 const handleAdjustmentQtyChange = (adjId: string, value: string) => { const numValue = value === '' || value === '-' ? 0 : Number(value); setAdjustments((prev) => prev.map((a) => (a.id === adjId ? { ...a, quantity: numValue } : a)) ); }; // 취소 핸들러 - 등록 모드면 목록으로, 수정 모드면 상세로 이동 const handleCancel = () => { if (isNewMode) { router.push('/ko/material/receiving-management'); } else { router.push(`/ko/material/receiving-management/${id}?mode=view`); } }; // ===== 읽기 전용 필드 렌더링 ===== const renderReadOnlyField = (label: string, value: string | number | undefined, isEditModeStyle = false) => (
{isEditModeStyle ? (
{value || '-'}
) : (
{value || '-'}
)}
); // ===== 상세 보기 콘텐츠 ===== const renderViewContent = useCallback(() => { if (!detail) return null; return (
{/* 기본 정보 */} 기본 정보
{renderReadOnlyField('입고번호', detail.materialNo)} {renderReadOnlyField('자재번호', detail.supplierMaterialNo)} {renderReadOnlyField('원자재로트', detail.lotNo)} {renderReadOnlyField('품목코드', detail.itemCode)} {renderReadOnlyField('품목명', detail.itemName)} {renderReadOnlyField('규격', detail.specification)} {renderReadOnlyField('단위', detail.unit)} {renderReadOnlyField('발주처', detail.supplier)} {renderReadOnlyField('제조사', detail.manufacturer)} {renderReadOnlyField('입고수량', detail.receivingQty)} {renderReadOnlyField('입고일', detail.receivingDate)} {renderReadOnlyField('작성자', detail.createdBy)} {renderReadOnlyField('상태', detail.status === 'receiving_pending' ? '입고대기' : detail.status === 'completed' ? '입고완료' : detail.status === 'inspection_completed' ? '검사완료' : detail.status )}
{/* 비고 - 전체 너비 */}
{renderReadOnlyField('비고', detail.remark)}
{/* 수입검사 정보 */} 수입검사 정보
{renderReadOnlyField('검사일', detail.inspectionDate)} {renderReadOnlyField('검사결과', detail.inspectionResult)}
{inspectionAttachments.length > 0 ? (
{inspectionAttachments.map((att) => { const fileName = att.file?.display_name || att.file?.original_name || `file-${att.file_id}`; const fileSize = att.file?.file_size; const isImage = att.file?.mime_type?.startsWith('image/'); const downloadUrl = `/api/proxy/files/${att.file_id}/download`; return (
{isImage && att.file?.file_path ? ( {fileName} ) : ( )}

{fileName}

{fileSize && (

{fileSize < 1024 * 1024 ? `${(fileSize / 1024).toFixed(1)} KB` : `${(fileSize / (1024 * 1024)).toFixed(1)} MB`}

)}
다운로드
); })}
) : (
첨부된 파일이 없습니다.
)}
{/* 재고 조정 */} 재고 조정
No 조정일시 증감 수량 검수자 {adjustments.length > 0 ? ( adjustments.map((adj, idx) => ( {idx + 1} {adj.adjustmentDate} {adj.quantity} {adj.inspector} )) ) : ( 재고 조정 이력이 없습니다. )}
); }, [detail, adjustments, inspectionAttachments]); // ===== 등록/수정 폼 콘텐츠 ===== const renderFormContent = useCallback(() => { return (
{/* 기본 정보 */} 기본 정보
{/* 입고번호 - 읽기전용 */} {renderReadOnlyField('입고번호', formData.materialNo, true)} {/* 자재번호 (거래처) - 수정 가능 */}
setFormData((prev) => ({ ...prev, supplierMaterialNo: e.target.value }))} placeholder="거래처 자재번호" />
{/* 원자재로트 - 수정 가능 */}
setFormData((prev) => ({ ...prev, lotNo: e.target.value }))} placeholder="원자재로트를 입력하세요" />
{/* 품목코드 - 검색 모달 선택 */}
setIsItemSearchOpen(true)} />
{/* 품목명 - 읽기전용 */} {renderReadOnlyField('품목명', formData.itemName, true)} {/* 규격 - 읽기전용 */} {renderReadOnlyField('규격', formData.specification, true)} {/* 단위 - 읽기전용 */} {renderReadOnlyField('단위', formData.unit, true)} {/* 발주처 - 검색 모달 선택 */}
setIsSupplierSearchOpen(true)} />
{/* 제조사 - 수정가능 */}
handleInputChange('manufacturer', e.target.value)} className="mt-1.5" placeholder="제조사 입력" />
{/* 입고수량 - 수정가능 */}
handleInputChange('receivingQty', e.target.value ? Number(e.target.value) : undefined)} className="mt-1.5" placeholder="입고수량 입력" />
{/* 입고일 - 수정가능 */}
handleInputChange('receivingDate', date)} />
{/* 작성자 - 읽기전용 */} {renderReadOnlyField('작성자', formData.createdBy, true)} {/* 상태 - 수정가능 (셀렉트) */}
{/* 비고 - 수정가능 */}
handleInputChange('remark', e.target.value)} className="mt-1.5" placeholder="비고 입력" />
{/* 수입검사 정보 */} 수입검사 정보
{/* 검사일 - 읽기전용 */} {renderReadOnlyField('검사일', formData.inspectionDate, true)} {/* 검사결과 - 읽기전용 */} {renderReadOnlyField('검사결과', formData.inspectionResult, true)}
{/* 업체 제공 성적서 자료 - 파일 업로드 */}
클릭하여 파일을 찾거나, 마우스로 파일을 끌어오세요.
{/* 재고 조정 */} 재고 조정
No 조정일시 증감 수량 검수자 {adjustments.length > 0 ? ( adjustments.map((adj, idx) => ( {idx + 1} { setAdjustments((prev) => prev.map((a) => a.id === adj.id ? { ...a, adjustmentDate: date } : a ) ); }} size="sm" /> handleAdjustmentQtyChange(adj.id, e.target.value)} className="h-8 text-sm text-center w-[100px] mx-auto" placeholder="0" /> {adj.inspector} )) ) : ( 재고 조정 이력이 없습니다. )}
); }, [formData, adjustments]); // ===== 커스텀 헤더 액션 (view/edit 모드) ===== // 수정 버튼은 IntegratedDetailTemplate의 DetailActions에서 아이콘으로 제공하므로 중복 제거 // 수입검사하기 버튼은 수입검사 성적서 템플릿이 있는 품목만 표시 const customHeaderActions = (isViewMode || isEditMode) && detail && hasInspectionTemplate ? (
) : undefined; // 에러 상태 표시 (view/edit 모드에서만) if (!isNewMode && !isLoading && (error || !detail)) { return ( ); } // 동적 config 생성 const dynamicConfig = { ...receivingConfig, title: isViewMode ? '입고 상세' : '입고', description: isNewMode ? '새로운 입고를 등록합니다' : isEditMode ? '입고 정보를 수정합니다' : '입고 상세를 관리합니다', actions: { ...receivingConfig.actions, showEdit: isViewMode, showDelete: false, }, }; return ( <> ) || {}} itemId={isNewMode ? undefined : id} isLoading={isLoading} headerActions={customHeaderActions} renderView={() => renderViewContent()} renderForm={() => renderFormContent()} onSubmit={async () => { return await handleSave(); }} onCancel={handleCancel} /> {/* 품목 검색 모달 */} { setFormData((prev) => ({ ...prev, itemCode: item.code, itemName: item.name, specification: item.specification || '', })); }} /> {/* 발주처 검색 모달 - TODO: SupplierSearchModal 컴포넌트 생성 필요 { setFormData((prev) => ({ ...prev, supplier: supplier.name, })); }} /> */} {/* 수입검사 성적서 모달 (읽기 전용) */} setIsInspectionModalOpen(false)} document={{ id: 'import-inspection', type: 'import', title: '수입검사 성적서', count: 0, }} documentItem={{ id: id, title: detail?.itemName || '수입검사 성적서', date: detail?.inspectionDate || '', code: detail?.lotNo || '', }} // 수입검사 템플릿 로드용 props itemId={detail?.itemId} itemName={detail?.itemName} specification={detail?.specification} supplier={detail?.supplier} inspector={getLoggedInUserName()} inspectorDept={getLoggedInUser().department} lotSize={detail?.receivingQty} materialNo={detail?.materialNo} readOnly={true} /> {/* 수입검사 입력 모달 */} ); }