/** * DynamicItemForm - 품목기준관리 API 기반 동적 품목 등록 폼 * * 기존 ItemForm과 100% 동일한 디자인 유지 */ 'use client'; import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { cn } from '@/lib/utils'; import { useMenuStore } from '@/stores/menuStore'; import { Save, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { FormSectionSkeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card'; import ItemTypeSelect from '../ItemTypeSelect'; import BendingDiagramSection from '../ItemForm/BendingDiagramSection'; import { DrawingCanvas } from '../DrawingCanvas'; import { useFormStructure, useDynamicFormState, useConditionalDisplay, useFieldDetection, useItemCodeGeneration, usePartTypeHandling, useFileHandling } from './hooks'; import { DynamicFieldRenderer } from './fields'; import { DynamicBOMSection } from './sections'; import { FormHeader, ValidationAlert, FileUploadFields, DuplicateCodeDialog } from './components'; import type { DynamicItemFormProps, DynamicFormData, BOMLine, BOMSearchState } from './types'; import type { ItemType, BendingDetail } from '@/types/item'; import type { ItemFieldResponse } from '@/types/item-master-api'; import { uploadItemFile, ItemFileType, checkItemCodeDuplicate, DuplicateCheckResult } from '@/lib/api/items'; import { DuplicateCodeError } from '@/lib/api/error-handler'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; /** * 메인 DynamicItemForm 컴포넌트 */ export default function DynamicItemForm({ mode, itemType: initialItemType, itemId: propItemId, initialData, initialBomLines, onSubmit, }: DynamicItemFormProps) { const router = useRouter(); const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); // 품목 유형 상태 (변경 가능) const [selectedItemType, setSelectedItemType] = useState(initialItemType || ''); // 폼 구조 로드 (품목 유형에 따라) const { structure, isLoading, error: structureError, unitOptions } = useFormStructure( selectedItemType as 'FG' | 'PT' | 'SM' | 'RM' | 'CS' ); // 폼 상태 관리 const { formData, errors, isSubmitting, setFieldValue, validateAll, handleSubmit, resetForm, } = useDynamicFormState(initialData); // BOM 상태 관리 const [bomLines, setBomLines] = useState([]); const [bomSearchStates, setBomSearchStates] = useState>({}); // 절곡품 전개도 상태 관리 (PT - 절곡 부품 전용) const [bendingDiagramInputMethod, setBendingDiagramInputMethod] = useState<'file' | 'drawing'>('file'); const [bendingDiagram, setBendingDiagram] = useState(''); const [bendingDiagramFile, setBendingDiagramFile] = useState(null); const [isDrawingOpen, setIsDrawingOpen] = useState(false); const [bendingDetails, setBendingDetails] = useState([]); const [widthSum, setWidthSum] = useState(''); // FG(제품) 전용 파일 업로드 상태 관리 const [specificationFile, setSpecificationFile] = useState(null); const [certificationFile, setCertificationFile] = useState(null); // 파일 처리 훅 (기존 파일 로드, 다운로드, 삭제) const { existingBendingDiagram, existingBendingDiagramFileId, existingSpecificationFile, existingSpecificationFileName, existingSpecificationFileId, existingCertificationFile, existingCertificationFileName, existingCertificationFileId, isDeletingFile, setExistingBendingDiagram: _setExistingBendingDiagram, setExistingBendingDiagramFileId: _setExistingBendingDiagramFileId, handleFileDownload, handleDeleteFile: handleDeleteFileFromHook, loadedBendingDetails, loadedWidthSum, } = useFileHandling({ mode, initialData, propItemId, selectedItemType, }); // 품목코드 중복 체크 상태 관리 const [showDuplicateDialog, setShowDuplicateDialog] = useState(false); const [duplicateCheckResult, setDuplicateCheckResult] = useState(null); const [_pendingSubmitData, setPendingSubmitData] = useState(null); // 훅에서 로드한 bendingDetails/widthSum을 로컬 상태와 동기화 (edit 모드) useEffect(() => { if (loadedBendingDetails.length > 0) { setBendingDetails(loadedBendingDetails); } if (loadedWidthSum) { setWidthSum(loadedWidthSum); } }, [loadedBendingDetails, loadedWidthSum]); // initialBomLines prop으로 BOM 데이터 로드 (edit 모드) // 2025-12-12: edit 페이지에서 별도로 전달받은 BOM 데이터 사용 useEffect(() => { if (mode === 'edit' && initialBomLines && initialBomLines.length > 0) { setBomLines(initialBomLines); } }, [mode, initialBomLines]); // 파일 삭제 래퍼 (훅의 handleDeleteFile에 콜백 전달) const handleDeleteFile = async (fileType: ItemFileType) => { await handleDeleteFileFromHook(fileType, { onBendingDiagramDeleted: () => setBendingDiagram(''), }); }; // 조건부 표시 관리 const { shouldShowSection, shouldShowField } = useConditionalDisplay(structure, formData); // PT(부품) 품목코드 자동생성용 - 기존 품목코드 목록 const [existingItemCodes, setExistingItemCodes] = useState([]); // PT(부품) 선택 시 기존 품목코드 목록 조회 useEffect(() => { if (selectedItemType === 'PT') { // PT 품목 목록 조회하여 기존 코드 수집 const fetchExistingCodes = async () => { try { const response = await fetch('/api/proxy/items?type=PT&size=1000'); const result = await response.json(); if (result.success && result.data?.data) { const codes = result.data.data .map((item: { code?: string; item_code?: string }) => item.code || item.item_code || '') .filter((code: string) => code); setExistingItemCodes(codes); } } catch (err) { console.error('[DynamicItemForm] PT 품목코드 조회 실패:', err); setExistingItemCodes([]); } }; fetchExistingCodes(); } else { setExistingItemCodes([]); } }, [selectedItemType]); // 품목 유형 변경 시 폼 초기화 (create 모드) useEffect(() => { if (selectedItemType && mode === 'create' && structure) { // 기본값 설정 const defaults: DynamicFormData = { item_type: selectedItemType, }; // 구조에서 기본값 추출 structure.sections.forEach((section) => { section.fields.forEach((f) => { const field = f.field; const fieldKey = field.field_key || `field_${field.id}`; if (field.default_value !== null && field.default_value !== undefined) { defaults[fieldKey] = field.default_value; } }); }); structure.directFields.forEach((f) => { const field = f.field; const fieldKey = field.field_key || `field_${field.id}`; if (field.default_value !== null && field.default_value !== undefined) { defaults[fieldKey] = field.default_value; } }); resetForm(defaults); // BOM 상태 초기화 - 빈 상태로 시작 (사용자가 추가 버튼으로 행 추가) setBomLines([]); setBomSearchStates({}); } }, [selectedItemType, structure, mode, resetForm]); // Edit 모드: initialData를 폼에 직접 로드 // 2025-12-09: field_key 통일로 복잡한 매핑 로직 제거 // 백엔드에서 field_key 그대로 응답하므로 직접 사용 가능 const [isEditDataMapped, setIsEditDataMapped] = useState(false); useEffect(() => { if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return; // structure의 field_key들 확인 const fieldKeys: string[] = []; structure.sections.forEach((section) => { section.fields.forEach((f) => { fieldKeys.push(f.field.field_key || `field_${f.field.id}`); }); }); // field_key가 통일되었으므로 initialData를 그대로 사용 // 기존 레거시 데이터(98_unit 형식)도 그대로 동작 resetForm(initialData); setIsEditDataMapped(true); }, [mode, structure, initialData, isEditDataMapped, resetForm]); // 모든 필드 목록 (밸리데이션용) - 숨겨진 섹션/필드 제외 const allFields = useMemo(() => { if (!structure) return []; const fields: ItemFieldResponse[] = []; // 표시되는 섹션의 표시되는 필드만 포함 structure.sections.forEach((section) => { // 섹션이 숨겨져 있으면 스킵 (조건부 표시) if (!shouldShowSection(section.section.id)) return; section.fields.forEach((f) => { // 필드가 숨겨져 있으면 스킵 (조건부 표시) if (!shouldShowField(f.field.id)) return; fields.push(f.field); }); }); // 직접 필드도 필터링 (조건부 표시) structure.directFields.forEach((f) => { if (!shouldShowField(f.field.id)) return; fields.push(f.field); }); return fields; }, [structure, shouldShowSection, shouldShowField]); // 부품 유형 및 BOM 필드 탐지 (커스텀 훅으로 분리) const { partTypeFieldKey, selectedPartType, isBendingPart, isAssemblyPart, isPurchasedPart, bomRequiredFieldKey, } = useFieldDetection({ structure, selectedItemType, formData, }); // 품목코드 자동생성 관련 정보 (커스텀 훅으로 분리) const { hasAutoItemCode, itemNameKey, allSpecificationKeys: _allSpecificationKeys, statusFieldKey, activeSpecificationKey, bendingFieldKeys, autoBendingItemCode, allCategoryKeysWithIds, hasAssemblyFields: _hasAssemblyFields, assemblyFieldKeys: _assemblyFieldKeys, autoAssemblyItemName, autoAssemblySpec, purchasedFieldKeys, autoPurchasedItemCode, autoGeneratedItemCode, } = useItemCodeGeneration({ structure, selectedItemType, formData, isBendingPart, isAssemblyPart, isPurchasedPart, existingItemCodes, shouldShowSection, shouldShowField, }); // 부품 유형 변경 시 필드 초기화 처리 (커스텀 훅으로 분리) usePartTypeHandling({ structure, selectedItemType, partTypeFieldKey, selectedPartType, itemNameKey, setFieldValue, formData, bendingFieldKeys, isBendingPart, allCategoryKeysWithIds, mode, bendingDetails, }); // 품목 유형 변경 핸들러 const handleItemTypeChange = (type: ItemType) => { setSelectedItemType(type); }; // 실제 저장 로직 (중복 체크 후 호출) const executeSubmit = async (submitData: DynamicFormData) => { try { await handleSubmit(async () => { // 품목 저장 (ID 반환) const result = await onSubmit(submitData); const itemId = result?.id; // 파일 업로드 (품목 ID가 있을 때만) if (itemId) { const fileUploadErrors: string[] = []; // PT (절곡/조립) 전개도 이미지 업로드 if (selectedItemType === 'PT' && (isBendingPart || isAssemblyPart) && bendingDiagramFile) { try { await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', { fieldKey: 'bending_diagram', // 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록 fileId: existingBendingDiagramFileId ?? undefined, bendingDetails: bendingDetails.length > 0 ? bendingDetails.map(d => ({ angle: d.aAngle || 0, length: d.input || 0, type: d.shaded ? 'shaded' : 'normal', })) : undefined, }); } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[DynamicItemForm] 전개도 파일 업로드 실패:', error); fileUploadErrors.push('전개도 이미지'); } } // FG (제품) 시방서 업로드 if (selectedItemType === 'FG' && specificationFile) { try { await uploadItemFile(itemId, specificationFile, 'specification', { fieldKey: 'specification_file', // 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록 fileId: existingSpecificationFileId ?? undefined, }); } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[DynamicItemForm] 시방서 파일 업로드 실패:', error); fileUploadErrors.push('시방서'); } } // FG (제품) 인정서 업로드 if (selectedItemType === 'FG' && certificationFile) { try { // formData에서 인정서 관련 필드 추출 const certNumber = Object.entries(formData).find(([key]) => key.includes('certification_number') || key.includes('인정번호') )?.[1] as string | undefined; const certStartDate = Object.entries(formData).find(([key]) => key.includes('certification_start') || key.includes('인정_유효기간_시작') )?.[1] as string | undefined; const certEndDate = Object.entries(formData).find(([key]) => key.includes('certification_end') || key.includes('인정_유효기간_종료') )?.[1] as string | undefined; await uploadItemFile(itemId, certificationFile, 'certification', { fieldKey: 'certification_file', // 수정 모드: 기존 파일 ID가 있으면 덮어쓰기, 없으면 새 파일 등록 fileId: existingCertificationFileId ?? undefined, certificationNumber: certNumber, certificationStartDate: certStartDate, certificationEndDate: certEndDate, }); } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[DynamicItemForm] 인정서 파일 업로드 실패:', error); fileUploadErrors.push('인정서'); } } // 파일 업로드 실패 경고 (품목은 저장됨) if (fileUploadErrors.length > 0) { console.warn('[DynamicItemForm] 일부 파일 업로드 실패:', fileUploadErrors.join(', ')); // 품목은 저장되었으므로 경고만 표시하고 진행 alert(`품목이 저장되었습니다.\n\n일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}\n수정 화면에서 다시 업로드해 주세요.`); } } router.push('/production/screen-production'); router.refresh(); }); } catch (error) { // 2025-12-11: 백엔드에서 중복 에러 반환 시 다이얼로그 표시 // 사전 체크를 우회하거나 동시 등록 시에도 안전하게 처리 if (error instanceof DuplicateCodeError) { console.warn('[DynamicItemForm] 저장 시점 중복 에러 감지:', error); setDuplicateCheckResult({ isDuplicate: true, duplicateId: error.duplicateId, }); setPendingSubmitData(submitData); setShowDuplicateDialog(true); return; } // 그 외 에러는 상위로 전파 throw error; } }; // 폼 제출 핸들러 const handleFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); // 밸리데이션 - 조건부 표시로 숨겨진 필드는 이미 allFields에서 제외됨 // 2025-12-03: 연동 드롭다운 로직 제거 - 단순화 const isValid = validateAll(allFields); if (!isValid) { return; } // 2025-12-09: field_key 통일로 변환 로직 제거 // formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용 // is_active 필드만 boolean 변환 (드롭다운 값 → boolean) const convertedData: Record = {}; Object.entries(formData).forEach(([key, value]) => { if (key === 'is_active' || key.endsWith('_is_active')) { // "활성", true, "true", "1", 1 등을 true로, 나머지는 false로 const isActive = value === true || value === 'true' || value === '1' || value === 1 || value === '활성' || value === 'active'; convertedData[key] = isActive; } else { convertedData[key] = value; } }); // 품목명 값 추출 (품목코드와 품목명 모두 필요) // 2025-12-04: 절곡 부품은 별도 품목명 필드(bendingFieldKeys.itemName) 사용 const effectiveItemNameKeyForSubmit = isBendingPart && bendingFieldKeys.itemName ? bendingFieldKeys.itemName : itemNameKey; const itemNameValue = effectiveItemNameKeyForSubmit ? (formData[effectiveItemNameKeyForSubmit] as string) || '' : ''; // 조립/절곡/구매 부품 자동생성 값 결정 // 조립 부품: 품목명 = "품목명 가로x세로", 규격 = "가로x세로x길이" // 절곡 부품: 품목명 = bendingFieldKeys.itemName에서 선택한 값, 규격 = 없음 (품목코드로 대체) // 구매 부품: 품목명 = purchasedFieldKeys.itemName에서 선택한 값 let finalName: string; let finalSpec: string | undefined; if (isAssemblyPart && autoAssemblyItemName) { // 조립 부품: 자동생성 품목명/규격 사용 finalName = autoAssemblyItemName; finalSpec = autoAssemblySpec; } else if (isBendingPart) { // 절곡 부품: bendingFieldKeys.itemName의 값 사용 finalName = itemNameValue || convertedData.name || ''; finalSpec = convertedData.spec; } else if (isPurchasedPart) { // 구매 부품: purchasedFieldKeys.itemName의 값 사용 const purchasedItemNameValue = purchasedFieldKeys.itemName ? (formData[purchasedFieldKeys.itemName] as string) || '' : ''; finalName = purchasedItemNameValue || convertedData.name || ''; finalSpec = convertedData.spec; } else { // 기타: 기존 로직 finalName = convertedData.name || itemNameValue; finalSpec = convertedData.spec; } // // 품목코드 결정 // 2025-12-11: 수정 모드에서는 기존 코드 유지 (자동생성으로 코드가 변경되는 버그 수정) // 생성 모드에서만 자동생성 코드 사용 let finalCode: string; if (mode === 'edit' && initialData?.code) { // 수정 모드: DB에서 받은 기존 코드 유지 finalCode = initialData.code as string; } else if (isBendingPart && autoBendingItemCode) { // 생성 모드: 절곡 부품 자동생성 finalCode = autoBendingItemCode; } else if (isPurchasedPart && autoPurchasedItemCode) { // 생성 모드: 구매 부품 자동생성 finalCode = autoPurchasedItemCode; } else if (hasAutoItemCode && autoGeneratedItemCode) { // 생성 모드: 일반 자동생성 finalCode = autoGeneratedItemCode; } else { finalCode = convertedData.code || itemNameValue; } // 품목 유형 및 BOM 데이터 추가 const submitData = { ...convertedData, // 백엔드 필드명 사용 product_type: selectedItemType, // item_type → product_type // 2025-12-03: 조립 부품 자동생성 품목명/규격 사용 // 2025-12-04: 절곡 부품도 자동생성 품목코드 사용 name: finalName, // 조립 부품: 품목명 가로x세로, 절곡 부품: 품목명 필드값, 기타: 품목명 필드값 spec: finalSpec, // 조립 부품: 가로x세로x길이, 기타: 규격 필드값 code: finalCode, // 절곡 부품: autoBendingItemCode, 기타: autoGeneratedItemCode // BOM 데이터를 배열로 포함 (백엔드는 child_item_id, child_item_type, quantity만 저장) bom: bomLines.map((line) => ({ child_item_id: line.childItemId ? Number(line.childItemId) : null, child_item_type: line.childItemType || 'PRODUCT', // PRODUCT(FG/PT) 또는 MATERIAL(SM/RM/CS) quantity: line.quantity || 1, })).filter(item => item.child_item_id !== null), // child_item_id 없는 항목 제외 // 절곡품 전개도 데이터 (PT - 절곡 부품 전용) ...(selectedItemType === 'PT' && isBendingPart ? { part_type: 'BENDING', bending_diagram: bendingDiagram || null, bending_details: bendingDetails.length > 0 ? bendingDetails : null, width_sum: widthSum || null, } : {}), // 조립품 전개도 데이터 (PT - 조립 부품 전용) ...(selectedItemType === 'PT' && isAssemblyPart ? { part_type: 'ASSEMBLY', bending_diagram: bendingDiagram || null, // 조립품도 동일한 전개도 필드 사용 } : {}), // 구매품 데이터 (PT - 구매 부품 전용) ...(selectedItemType === 'PT' && isPurchasedPart ? { part_type: 'PURCHASED', } : {}), // FG(제품)은 단위 필드가 없으므로 기본값 'EA' 설정 ...(selectedItemType === 'FG' && !convertedData.unit ? { unit: 'EA', } : {}), } as DynamicFormData; // // 2025-12-11: 품목코드 중복 체크 (조립/절곡 부품만 해당) // PT-조립부품, PT-절곡부품은 품목코드가 자동생성되므로 중복 체크 필요 const needsDuplicateCheck = selectedItemType === 'PT' && (isAssemblyPart || isBendingPart) && finalCode; if (needsDuplicateCheck) { // 수정 모드에서는 자기 자신 제외 (propItemId) const excludeId = mode === 'edit' ? propItemId : undefined; const duplicateResult = await checkItemCodeDuplicate(finalCode, excludeId); if (duplicateResult.isDuplicate) { // 중복 발견 → 다이얼로그 표시 setDuplicateCheckResult(duplicateResult); setPendingSubmitData(submitData); setShowDuplicateDialog(true); return; // 저장 중단, 사용자 선택 대기 } } // 중복 없음 → 바로 저장 await executeSubmit(submitData); }; // 중복 다이얼로그에서 "중복 품목 수정" 버튼 클릭 핸들러 const handleGoToEditDuplicate = () => { if (duplicateCheckResult?.duplicateId) { setShowDuplicateDialog(false); // 2025-12-11: 수정 페이지 URL 형식 맞춤 // /items/{code}/edit?type={itemType}&id={itemId} // duplicateItemType이 없으면 현재 선택된 품목 유형 사용 const itemType = duplicateCheckResult.duplicateItemType || selectedItemType || 'PT'; const itemId = duplicateCheckResult.duplicateId; // code는 없으므로 id를 path에 사용 (edit 페이지에서 id 쿼리 파라미터로 조회) router.push(`/production/screen-production/${itemId}?mode=edit&type=${itemType}&id=${itemId}`); } }; // 중복 다이얼로그에서 "취소" 버튼 클릭 핸들러 const handleCancelDuplicate = () => { setShowDuplicateDialog(false); setDuplicateCheckResult(null); setPendingSubmitData(null); }; // 로딩 상태 if (isLoading && selectedItemType) { return ; } // 에러 상태 if (structureError) { return ( ⚠️ 폼 구조를 불러오는데 실패했습니다: {structureError} ); } // 섹션 정렬 const sortedSections = structure ? [...structure.sections].sort((a, b) => a.orderNo - b.orderNo) : []; // 직접 필드 정렬 const sortedDirectFields = structure ? [...structure.directFields].sort((a, b) => a.orderNo - b.orderNo) : []; // 일반 섹션들 (BOM 제외) - 기본 정보 카드에 통합할 섹션들 const normalSections = sortedSections.filter((s) => s.section.type !== 'bom'); // BOM 섹션 - 별도 카드로 렌더링 const bomSection = sortedSections.find((s) => s.section.type === 'bom'); // 첫 번째 일반 섹션 (기본 필드용) const firstDefaultSection = normalSections[0]; // 나머지 일반 섹션들 (하위 섹션으로 렌더링) const additionalSections = normalSections.slice(1); // 통합 섹션의 필드 정렬 const firstSectionFields = firstDefaultSection ? [...firstDefaultSection.fields].sort((a, b) => a.orderNo - b.orderNo) : []; return (
{/* Validation 에러 Alert */} {/* 헤더 */} {/* 기본 정보 - 목업과 동일한 레이아웃 (모든 필드 한 줄씩) */} 기본 정보 {/* 품목 유형 선택 */}

* 품목 유형에 따라 입력 항목이 다릅니다

{/* 직접 필드 (페이지에 직접 연결된 필드) */} {selectedItemType && sortedDirectFields.map((dynamicField) => { const field = dynamicField.field; const fieldKey = field.field_key || `field_${field.id}`; // 필드 조건부 표시 체크 if (!shouldShowField(field.id)) { return null; } return ( setFieldValue(fieldKey, value)} error={errors[fieldKey]} disabled={isSubmitting} unitOptions={unitOptions} formData={formData} /> ); })} {/* 첫 번째 섹션의 필드 렌더링 */} {selectedItemType && firstSectionFields.map((dynamicField) => { const field = dynamicField.field; const fieldKey = field.field_key || `field_${field.id}`; // 필드 조건부 표시 체크 (백엔드 설정 그대로 유지) if (!shouldShowField(field.id)) { return null; } const isSpecField = fieldKey === activeSpecificationKey; const isStatusField = fieldKey === statusFieldKey; // 품목명 필드인지 체크 (FG 품목코드 자동생성 위치) const isItemNameField = fieldKey === itemNameKey; // 비고 필드인지 체크 (절곡부품 품목코드 자동생성 위치) const fieldName = field.field_name || ''; const isNoteField = fieldKey.includes('note') || fieldKey.includes('비고') || fieldName.includes('비고') || fieldName === '비고'; // 인정 유효기간 종료일 필드인지 체크 (FG 시방서/인정서 파일 업로드 위치) const isCertEndDateField = fieldKey.includes('certification_end') || fieldKey.includes('인정_유효기간_종료') || fieldName.includes('인정 유효기간 종료') || fieldName.includes('유효기간 종료'); // 절곡부품 박스 스타일링 (재질, 폭합계, 모양&길이) const isBendingBoxField = isBendingPart && ( fieldKey === bendingFieldKeys.material || fieldKey === bendingFieldKeys.widthSum || fieldKey === bendingFieldKeys.shapeLength ); const isFirstBendingBoxField = isBendingPart && fieldKey === bendingFieldKeys.material; const isLastBendingBoxField = isBendingPart && fieldKey === bendingFieldKeys.shapeLength; return (
setFieldValue(fieldKey, value)} error={errors[fieldKey]} disabled={isSubmitting} unitOptions={unitOptions} formData={formData} /> {/* 규격 필드 바로 다음에 품목코드 자동생성 필드 표시 (절곡부품 제외) */} {isSpecField && hasAutoItemCode && !isBendingPart && (

{selectedItemType === 'PT' ? "* 품목코드는 '영문약어-순번' 형식으로 자동 생성됩니다" : "* 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다"}

)} {/* 품목 상태 필드 하단 안내 메시지 */} {isStatusField && (

* 비활성 시 품목 사용이 제한됩니다

)} {/* 비고 필드 다음에 절곡부품 품목코드 자동생성 */} {isNoteField && isBendingPart && (

* 절곡 부품 품목코드는 '품목명+종류+길이축약' 형식으로 자동 생성됩니다 (예: RM30)

)} {/* 비고 필드 다음에 구매부품(전동개폐기) 품목코드 자동생성 */} {isNoteField && isPurchasedPart && (

* 품목코드는 '품목명+용량+전원' 형식으로 자동 생성됩니다 (예: 전동개폐기150KG380V)

)} {/* FG(제품) 전용: 품목명 필드 다음에 품목코드 자동생성 */} {isItemNameField && selectedItemType === 'FG' && (

* 제품(FG)의 품목코드는 품목명과 동일하게 설정됩니다

)} {/* FG(제품) 전용: 인정 유효기간 종료일 다음에 시방서/인정서 파일 업로드 */} {isCertEndDateField && selectedItemType === 'FG' && ( )}
); })} {/* 추가 섹션들 (기본 정보 카드 내에 하위 섹션으로 통합) */} {selectedItemType && additionalSections.map((section) => { // 조건부 표시 체크 if (!shouldShowSection(section.section.id)) { return null; } // 부품 유형에 따른 섹션 필터링 const sectionTitle = section.section.title || ''; const isPurchaseSection = sectionTitle.includes('구매 부품'); const isAssemblySectionTitle = sectionTitle.includes('조립 부품'); // 조립 부품 선택 시 구매 부품 섹션 숨김 if (isAssemblyPart && isPurchaseSection) { return null; } // 구매 부품 선택 시 조립 부품 섹션 숨김 if (!isAssemblyPart && !isBendingPart && isAssemblySectionTitle) { return null; } // 섹션 필드 정렬 const sectionFields = [...section.fields].sort((a, b) => a.orderNo - b.orderNo); return (
{/* 하위 섹션 제목 */}

{section.section.title}

{section.section.description && (

{section.section.description}

)} {/* 하위 섹션 필드들 */}
{sectionFields.map((dynamicField) => { const field = dynamicField.field; const fieldKey = field.field_key || `field_${field.id}`; // 필드 조건부 표시 체크 if (!shouldShowField(field.id)) { return null; } return ( setFieldValue(fieldKey, value)} error={errors[fieldKey]} disabled={isSubmitting} unitOptions={unitOptions} formData={formData} /> ); })}
); })}
{/* 품목 유형 선택 안내 경고 */} {!selectedItemType && ( ⚠️ 품목 유형을 먼저 선택해주세요 )} {/* 조립품 전개도 섹션 (PT - 조립 부품 전용) - 품목명 선택 시 표시 */} {selectedItemType === 'PT' && isAssemblyPart && ( setFieldValue(key, value)} isSubmitting={isSubmitting} existingBendingDiagram={existingBendingDiagram} existingBendingDiagramFileId={existingBendingDiagramFileId} onDeleteExistingFile={() => handleDeleteFile('bending_diagram')} isDeletingFile={isDeletingFile === 'bending_diagram'} /> )} {/* 절곡품 전개도 섹션 (PT - 절곡 부품 전용) */} {selectedItemType === 'PT' && isBendingPart && ( setFieldValue(key, value)} isSubmitting={isSubmitting} existingBendingDiagram={existingBendingDiagram} existingBendingDiagramFileId={existingBendingDiagramFileId} onDeleteExistingFile={() => handleDeleteFile('bending_diagram')} isDeletingFile={isDeletingFile === 'bending_diagram'} /> )} {/* BOM 섹션 (부품구성 필요 체크 시에만 표시) */} {selectedItemType && bomSection && (() => { // bomRequiredFieldKey는 useMemo에서 structure 기반으로 미리 계산됨 const bomValue = bomRequiredFieldKey ? formData[bomRequiredFieldKey] : undefined; const isBomRequired = bomValue === true || bomValue === 'true' || bomValue === '1' || bomValue === 1; // 디버깅 로그 // if (!isBomRequired) return null; return ( ); })()} {/* 전개도 그리기 다이얼로그 (절곡품/조립품 공용) */} { setBendingDiagram(imageData); // Base64 string을 File 객체로 변환 (업로드용) // 2025-12-06: 드로잉 방식에서도 파일 업로드 지원 try { // eslint-disable-next-line no-undef const byteString = atob(imageData.split(',')[1]); const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0]; const arrayBuffer = new ArrayBuffer(byteString.length); const uint8Array = new Uint8Array(arrayBuffer); for (let i = 0; i < byteString.length; i++) { uint8Array[i] = byteString.charCodeAt(i); } // eslint-disable-next-line no-undef const blob = new Blob([uint8Array], { type: mimeType }); const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType }); setBendingDiagramFile(file); } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error); } setIsDrawingOpen(false); }} initialImage={bendingDiagram} title={isAssemblyPart ? "조립품 전개도" : "절곡품 전개도"} description={isAssemblyPart ? "조립품 전개도(바라시)를 그리거나 편집합니다." : "절곡품 전개도를 그리거나 편집합니다." } /> {/* 품목코드 중복 확인 다이얼로그 */} {/* 하단 액션 버튼 (sticky) */}
); }