/** * DynamicItemForm - 품목기준관리 API 기반 동적 품목 등록 폼 * * 기존 ItemForm과 100% 동일한 디자인 유지 */ 'use client'; import { useState, useEffect, useMemo, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { Package, Save, X, FileText, Trash2, Download } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { PageLoadingSpinner } from '@/components/ui/loading-spinner'; import { Button } from '@/components/ui/button'; 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 } from './hooks'; import { DynamicFieldRenderer } from './fields'; import { DynamicBOMSection } from './sections'; import { generateItemCode, generateAssemblyItemNameSimple, generateAssemblySpecification, generateBendingItemCodeSimple, generatePurchasedItemCode, } from './utils/itemCodeGenerator'; import type { DynamicItemFormProps, DynamicFormData, DynamicSection, DynamicFieldValue, BOMLine, BOMSearchState, ItemSaveResult } from './types'; import type { ItemType, BendingDetail } from '@/types/item'; import type { ItemFieldResponse } from '@/types/item-master-api'; import { uploadItemFile, deleteItemFile, ItemFileType } from '@/lib/api/items'; /** * 헤더 컴포넌트 - 기존 FormHeader와 동일한 디자인 */ function FormHeader({ mode, selectedItemType, isSubmitting, onCancel, }: { mode: 'create' | 'edit'; selectedItemType: string; isSubmitting: boolean; onCancel: () => void; }) { return (

{mode === 'create' ? '품목 등록' : '품목 수정'}

품목 정보를 입력하세요

); } /** * 밸리데이션 에러 Alert - 기존 ValidationAlert와 동일한 디자인 */ function ValidationAlert({ errors }: { errors: Record }) { const errorCount = Object.keys(errors).length; if (errorCount === 0) { return null; } return (
⚠️
입력 내용을 확인해주세요 ({errorCount}개 오류)
    {Object.entries(errors).map(([fieldKey, errorMessage]) => (
  • {errorMessage}
  • ))}
); } /** * 동적 섹션 렌더러 */ function DynamicSectionRenderer({ section, formData, errors, onChange, disabled, unitOptions, autoGeneratedItemCode, shouldShowField, }: { section: DynamicSection; formData: DynamicFormData; errors: Record; onChange: (fieldKey: string, value: DynamicFieldValue) => void; disabled?: boolean; unitOptions: { label: string; value: string }[]; autoGeneratedItemCode?: string; shouldShowField?: (fieldId: number) => boolean; }) { // 필드를 order_no 기준 정렬 const sortedFields = [...section.fields].sort((a, b) => a.orderNo - b.orderNo); // 이 섹션에 item_name과 specification 필드가 둘 다 있는지 체크 // field_key가 "{id}_item_name" 형식으로 올 수 있어서 includes로 체크 const fieldKeys = sortedFields.map((f) => f.field.field_key || `field_${f.field.id}`); const hasItemName = fieldKeys.some((k) => k.includes('item_name')); const hasSpecification = fieldKeys.some((k) => k.includes('specification')); const shouldShowItemCode = hasItemName && hasSpecification && autoGeneratedItemCode !== undefined; return ( {section.section.title} {section.section.description && (

{section.section.description}

)}
{sortedFields.map((dynamicField) => { const field = dynamicField.field; const fieldKey = field.field_key || `field_${field.id}`; // 필드 조건부 표시 체크 if (shouldShowField && !shouldShowField(field.id)) { return null; } return ( onChange(fieldKey, value)} error={errors[fieldKey]} disabled={disabled} unitOptions={unitOptions} /> ); })} {/* 품목코드 자동생성 필드 (item_name + specification 있는 섹션에만 표시) */} {shouldShowItemCode && (

* 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다

)}
); } /** * 메인 DynamicItemForm 컴포넌트 */ export default function DynamicItemForm({ mode, itemType: initialItemType, itemId: propItemId, initialData, onSubmit, }: DynamicItemFormProps) { const router = useRouter(); // 품목 유형 상태 (변경 가능) 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); // 기존 파일 URL 상태 (edit 모드에서 사용) const [existingBendingDiagram, setExistingBendingDiagram] = useState(''); const [existingSpecificationFile, setExistingSpecificationFile] = useState(''); const [existingSpecificationFileName, setExistingSpecificationFileName] = useState(''); const [existingCertificationFile, setExistingCertificationFile] = useState(''); const [existingCertificationFileName, setExistingCertificationFileName] = useState(''); const [isDeletingFile, setIsDeletingFile] = useState(null); // initialData에서 기존 파일 정보 로드 (edit 모드) useEffect(() => { if (mode === 'edit' && initialData) { if (initialData.bending_diagram) { setExistingBendingDiagram(initialData.bending_diagram as string); } if (initialData.specification_file) { setExistingSpecificationFile(initialData.specification_file as string); setExistingSpecificationFileName((initialData.specification_file_name as string) || '시방서'); } if (initialData.certification_file) { setExistingCertificationFile(initialData.certification_file as string); setExistingCertificationFileName((initialData.certification_file_name as string) || '인정서'); } } }, [mode, initialData]); // Storage 경로를 전체 URL로 변환 const getStorageUrl = (path: string | undefined): string | null => { if (!path) return null; if (path.startsWith('http://') || path.startsWith('https://')) { return path; } const apiUrl = process.env.NEXT_PUBLIC_API_URL || ''; return `${apiUrl}/storage/${path}`; }; // 파일 삭제 핸들러 const handleDeleteFile = async (fileType: ItemFileType) => { if (!propItemId) return; const confirmMessage = fileType === 'bending_diagram' ? '전개도 이미지를' : fileType === 'specification' ? '시방서 파일을' : '인정서 파일을'; if (!confirm(`${confirmMessage} 삭제하시겠습니까?`)) return; try { setIsDeletingFile(fileType); await deleteItemFile(propItemId, fileType); // 상태 업데이트 if (fileType === 'bending_diagram') { setExistingBendingDiagram(''); setBendingDiagram(''); } else if (fileType === 'specification') { setExistingSpecificationFile(''); setExistingSpecificationFileName(''); } else if (fileType === 'certification') { setExistingCertificationFile(''); setExistingCertificationFileName(''); } alert('파일이 삭제되었습니다.'); } catch (error) { console.error('[DynamicItemForm] 파일 삭제 실패:', error); alert('파일 삭제에 실패했습니다.'); } finally { setIsDeletingFile(null); } }; // 조건부 표시 관리 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); // console.log('[DynamicItemForm] PT 기존 품목코드 로드:', codes.length, '개'); } } 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 모드: structure 로드 후 initialData를 field_key 형식으로 변환 // 2025-12-04: initialData 키(item_name)와 structure의 field_key(98_item_name)가 다른 문제 해결 const [isEditDataMapped, setIsEditDataMapped] = useState(false); useEffect(() => { if (mode !== 'edit' || !structure || !initialData) return; // 이미 매핑된 데이터가 formData에 있으면 스킵 (98_unit 같은 field_key 형식) // StrictMode 리렌더에서도 안전하게 동작 const hasFieldKeyData = Object.keys(formData).some(key => /^\d+_/.test(key)); if (hasFieldKeyData) { console.log('[DynamicItemForm] Edit mode: 이미 field_key 형식 데이터 있음, 매핑 스킵'); return; } console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format'); console.log('[DynamicItemForm] initialData:', initialData); // initialData의 간단한 키를 structure의 field_key로 매핑 // 예: { item_name: '테스트' } → { '98_item_name': '테스트' } const mappedData: DynamicFormData = {}; // field_key에서 실제 필드명 추출하는 함수 // 예: '98_item_name' → 'item_name', '110_품목명' → '품목명' const extractFieldName = (fieldKey: string): string => { const underscoreIndex = fieldKey.indexOf('_'); if (underscoreIndex > 0) { return fieldKey.substring(underscoreIndex + 1); } return fieldKey; }; // structure에서 모든 필드의 field_key 수집 const fieldKeyMap: Record = {}; // 간단한 키 → field_key 매핑 // 영문 → 한글 필드명 별칭 (API 응답 키 → structure field_name 매핑) // API는 영문 키(unit, note)로 응답하지만, structure field_key는 한글(단위, 비고) 포함 const fieldAliases: Record = { 'unit': '단위', 'note': '비고', 'remarks': '비고', // Material 모델은 remarks 사용 'item_name': '품목명', 'specification': '규격', 'description': '설명', }; structure.sections.forEach((section) => { section.fields.forEach((f) => { const field = f.field; const fieldKey = field.field_key || `field_${field.id}`; const simpleName = extractFieldName(fieldKey); fieldKeyMap[simpleName] = fieldKey; // field_name도 매핑에 추가 (한글 필드명 지원) if (field.field_name) { fieldKeyMap[field.field_name] = fieldKey; } }); }); structure.directFields.forEach((f) => { const field = f.field; const fieldKey = field.field_key || `field_${field.id}`; const simpleName = extractFieldName(fieldKey); fieldKeyMap[simpleName] = fieldKey; if (field.field_name) { fieldKeyMap[field.field_name] = fieldKey; } }); console.log('[DynamicItemForm] fieldKeyMap:', fieldKeyMap); // initialData를 field_key 형식으로 변환 Object.entries(initialData).forEach(([key, value]) => { // 이미 field_key 형식인 경우 그대로 사용 if (key.includes('_') && /^\d+_/.test(key)) { mappedData[key] = value; } // 간단한 키인 경우 field_key로 변환 else if (fieldKeyMap[key]) { mappedData[fieldKeyMap[key]] = value; } // 영문 → 한글 별칭으로 시도 (API 응답 키 → structure field_name) else if (fieldAliases[key] && fieldKeyMap[fieldAliases[key]]) { mappedData[fieldKeyMap[fieldAliases[key]]] = value; console.log(`[DynamicItemForm] 별칭 매핑: ${key} → ${fieldAliases[key]} → ${fieldKeyMap[fieldAliases[key]]}`); } // 매핑 없는 경우 그대로 유지 else { mappedData[key] = value; } }); // 추가: 폼 구조의 모든 필드를 순회하면서, initialData에서 해당 값 직접 찾아서 설정 // (fieldKeyMap에 매핑이 없는 경우를 위한 fallback) Object.entries(fieldKeyMap).forEach(([simpleName, fieldKey]) => { // 아직 매핑 안된 필드인데 initialData에 값이 있으면 설정 if (mappedData[fieldKey] === undefined && initialData[simpleName] !== undefined) { mappedData[fieldKey] = initialData[simpleName]; } }); // 추가: 영문 별칭을 역으로 검색하여 매핑 (한글 field_name → 영문 API 키) // 예: fieldKeyMap에 '단위'가 있고, initialData에 'unit'이 있으면 매핑 Object.entries(fieldAliases).forEach(([englishKey, koreanKey]) => { const targetFieldKey = fieldKeyMap[koreanKey]; if (targetFieldKey && mappedData[targetFieldKey] === undefined && initialData[englishKey] !== undefined) { mappedData[targetFieldKey] = initialData[englishKey]; console.log(`[DynamicItemForm] 별칭 fallback 매핑: ${englishKey} → ${koreanKey} → ${targetFieldKey}`); } }); console.log('========== [DynamicItemForm] Edit 모드 데이터 매핑 =========='); console.log('specification 관련 키:', Object.keys(mappedData).filter(k => k.includes('specification') || k.includes('규격'))); console.log('is_active 관련 키:', Object.keys(mappedData).filter(k => k.includes('active') || k.includes('상태'))); console.log('매핑된 데이터:', mappedData); console.log('=============================================================='); // 변환된 데이터로 폼 리셋 resetForm(mappedData); 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]); // 품목코드 자동생성 관련 필드 정보 // field_key 또는 field_name 기준으로 품목명/규격 필드 탐지 // 2025-12-03: 연동 드롭다운 로직 제거 - 조건부 섹션 표시로 대체 const { hasAutoItemCode, itemNameKey, allSpecificationKeys, statusFieldKey } = useMemo(() => { if (!structure) return { hasAutoItemCode: false, itemNameKey: '', allSpecificationKeys: [] as string[], statusFieldKey: '' }; let foundItemNameKey = ''; let foundStatusFieldKey = ''; const specificationKeys: string[] = []; // 모든 규격 필드 키 수집 const checkField = (fieldKey: string, field: ItemFieldResponse) => { const fieldName = field.field_name || ''; // 품목명 필드 탐지 (field_key 또는 field_name 기준) const isItemName = fieldKey.includes('item_name') || fieldName.includes('품목명'); if (isItemName && !foundItemNameKey) { foundItemNameKey = fieldKey; } // 규격 필드 탐지 // specification, standard, 규격, 사양 모두 지원 const isSpecification = fieldKey.includes('specification') || fieldKey.includes('standard') || fieldKey.includes('규격') || fieldName.includes('규격') || fieldName.includes('사양'); if (isSpecification) { specificationKeys.push(fieldKey); } // 품목 상태 필드 탐지 (is_active, status, 품목상태, 품목 상태) const isStatusField = fieldKey.includes('is_active') || fieldKey.includes('status') || fieldKey.includes('active') || fieldName.includes('품목상태') || fieldName.includes('품목 상태') || fieldName === '상태'; if (isStatusField && !foundStatusFieldKey) { foundStatusFieldKey = fieldKey; } }; structure.sections.forEach((section) => { section.fields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); }); structure.directFields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); return { // PT(부품)도 품목코드 자동생성 포함 hasAutoItemCode: !!foundItemNameKey, itemNameKey: foundItemNameKey, allSpecificationKeys: specificationKeys, statusFieldKey: foundStatusFieldKey, }; }, [structure]); // 현재 표시 중인 규격 필드 키 (조건부 표시 고려) // 2025-12-03: 조건부 표시로 숨겨진 필드는 제외하고, 실제 표시되는 규격 필드만 선택 const activeSpecificationKey = useMemo(() => { if (!structure || allSpecificationKeys.length === 0) return ''; // 모든 규격 필드 중 현재 표시 중인 첫 번째 필드 찾기 for (const section of structure.sections) { // 섹션이 숨겨져 있으면 스킵 if (!shouldShowSection(section.section.id)) continue; for (const f of section.fields) { const fieldKey = f.field.field_key || `field_${f.field.id}`; // 필드가 숨겨져 있으면 스킵 if (!shouldShowField(f.field.id)) continue; // 규격 필드인지 확인 if (allSpecificationKeys.includes(fieldKey)) { return fieldKey; } } } // 직접 필드에서도 찾기 for (const f of structure.directFields) { const fieldKey = f.field.field_key || `field_${f.field.id}`; if (!shouldShowField(f.field.id)) continue; if (allSpecificationKeys.includes(fieldKey)) { return fieldKey; } } // 표시 중인 규격 필드가 없으면 첫 번째 규격 필드 반환 (fallback) return allSpecificationKeys[0] || ''; }, [structure, allSpecificationKeys, shouldShowSection, shouldShowField]); // 부품 유형 필드 탐지 (PT 품목에서 절곡/조립/구매 부품 판별용) const { partTypeFieldKey, selectedPartType, isBendingPart, isAssemblyPart, isPurchasedPart } = useMemo(() => { if (!structure || selectedItemType !== 'PT') { return { partTypeFieldKey: '', selectedPartType: '', isBendingPart: false, isAssemblyPart: false, isPurchasedPart: false }; } let foundPartTypeKey = ''; // 모든 필드에서 부품 유형 필드 찾기 const checkField = (fieldKey: string, field: ItemFieldResponse) => { const fieldName = field.field_name || ''; // part_type, 부품유형, 부품 유형 등 탐지 const isPartType = fieldKey.includes('part_type') || fieldName.includes('부품유형') || fieldName.includes('부품 유형'); if (isPartType && !foundPartTypeKey) { foundPartTypeKey = fieldKey; } }; structure.sections.forEach((section) => { section.fields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); }); structure.directFields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); const currentPartType = (formData[foundPartTypeKey] as string) || ''; // "절곡 부품", "BENDING", "절곡부품" 등 다양한 형태 지원 const isBending = currentPartType.includes('절곡') || currentPartType.toUpperCase() === 'BENDING'; // "조립 부품", "ASSEMBLY", "조립부품" 등 다양한 형태 지원 const isAssembly = currentPartType.includes('조립') || currentPartType.toUpperCase() === 'ASSEMBLY'; // "구매 부품", "PURCHASED", "구매부품" 등 다양한 형태 지원 const isPurchased = currentPartType.includes('구매') || currentPartType.toUpperCase() === 'PURCHASED'; // console.log('[DynamicItemForm] 부품 유형 감지:', { partTypeFieldKey: foundPartTypeKey, currentPartType, isBending, isAssembly, isPurchased }); return { partTypeFieldKey: foundPartTypeKey, selectedPartType: currentPartType, isBendingPart: isBending, isAssemblyPart: isAssembly, isPurchasedPart: isPurchased, }; }, [structure, selectedItemType, formData]); // 이전 부품 유형 값 추적 (부품 유형 변경 감지용) const prevPartTypeRef = useRef(''); // 부품 유형 변경 시 조건부 표시 관련 필드 초기화 // 2025-12-04: 절곡 ↔ 조립 부품 전환 시 formData 값이 유지되어 // 조건부 표시가 잘못 트리거되는 버그 수정 // 2025-12-04: setTimeout으로 초기화를 다음 틱으로 미뤄서 Select 두 번 클릭 문제 해결 useEffect(() => { if (selectedItemType !== 'PT' || !partTypeFieldKey) return; const currentPartType = selectedPartType; const prevPartType = prevPartTypeRef.current; // 이전 값이 있고, 현재 값과 다른 경우에만 초기화 if (prevPartType && prevPartType !== currentPartType) { // console.log('[DynamicItemForm] 부품 유형 변경 감지:', prevPartType, '→', currentPartType); // setTimeout으로 다음 틱에서 초기화 실행 // → 부품 유형 Select 값 변경이 먼저 완료된 후 초기화 setTimeout(() => { // 조건부 표시 대상이 될 수 있는 필드들 수집 및 초기화 // (품목명, 재질, 종류, 폭 합계, 모양&길이 등) const fieldsToReset: string[] = []; // structure에서 조건부 표시 설정이 있는 필드들 찾기 if (structure) { structure.sections.forEach((section) => { section.fields.forEach((f) => { const field = f.field; const fieldKey = field.field_key || `field_${field.id}`; const fieldName = field.field_name || ''; // 부품 유형 필드는 초기화에서 제외 if (fieldKey === partTypeFieldKey) return; // 조건부 표시 트리거 필드 (display_condition이 있는 필드) if (field.display_condition) { fieldsToReset.push(fieldKey); } // 조건부 표시 대상 필드 (재질, 종류, 폭 합계, 모양&길이 등) const isBendingRelated = fieldName.includes('재질') || fieldName.includes('종류') || fieldName.includes('폭') || fieldName.includes('모양') || fieldName.includes('길이') || fieldKey.includes('material') || fieldKey.includes('category') || fieldKey.includes('width') || fieldKey.includes('shape') || fieldKey.includes('length'); if (isBendingRelated) { fieldsToReset.push(fieldKey); } }); }); // 품목명 필드도 초기화 (조건부 표시 트리거 역할) if (itemNameKey) { fieldsToReset.push(itemNameKey); } } // 중복 제거 후 초기화 const uniqueFields = [...new Set(fieldsToReset)]; // console.log('[DynamicItemForm] 초기화할 필드:', uniqueFields); uniqueFields.forEach((fieldKey) => { setFieldValue(fieldKey, ''); }); }, 0); } // 현재 값을 이전 값으로 저장 prevPartTypeRef.current = currentPartType; }, [selectedItemType, partTypeFieldKey, selectedPartType, structure, itemNameKey, setFieldValue]); // 절곡부품 전용 필드 탐지 (재질, 종류, 폭 합계, 모양&길이) // 2025-12-04: 조건부 표시 고려하여 종류 필드 선택 로직 개선 const { bendingFieldKeys, autoBendingItemCode, allCategoryKeysWithIds } = useMemo(() => { if (!structure || selectedItemType !== 'PT' || !isBendingPart) { return { bendingFieldKeys: { material: '', // 재질 category: '', // 종류 widthSum: '', // 폭 합계 shapeLength: '', // 모양&길이 itemName: '', // 품목명 (절곡부품 코드 생성용) }, autoBendingItemCode: '', allCategoryKeysWithIds: [] as Array<{ key: string; id: number }>, }; } let materialKey = ''; const categoryKeysWithIds: Array<{ key: string; id: number }> = []; // 종류 필드 + ID let widthSumKey = ''; let shapeLengthKey = ''; let bendingItemNameKey = ''; // 절곡부품용 품목명 키 const checkField = (fieldKey: string, field: ItemFieldResponse) => { const fieldName = field.field_name || ''; const lowerKey = fieldKey.toLowerCase(); // 절곡부품 품목명 필드 탐지 - bending_parts 우선 // 2025-12-04: 조립부품/절곡부품 품목명 필드가 모두 있을 때 절곡부품용 우선 선택 const isBendingItemNameField = lowerKey.includes('bending_parts') || lowerKey.includes('bending_item') || lowerKey.includes('절곡부품') || lowerKey.includes('절곡_부품') || fieldName.includes('절곡부품') || fieldName.includes('절곡 부품'); const isGeneralItemNameField = lowerKey.includes('item_name') || lowerKey.includes('품목명') || fieldName.includes('품목명') || fieldName === '품목명'; // bending_parts는 무조건 우선 (덮어쓰기) if (isBendingItemNameField) { // console.log('[checkField] 절곡부품 품목명 필드 발견!', { fieldKey, fieldName }); bendingItemNameKey = fieldKey; } // 일반 품목명은 아직 없을 때만 else if (isGeneralItemNameField && !bendingItemNameKey) { // console.log('[checkField] 일반 품목명 필드 발견!', { fieldKey, fieldName }); bendingItemNameKey = fieldKey; } // 재질 필드 if (lowerKey.includes('material') || lowerKey.includes('재질') || lowerKey.includes('texture') || fieldName.includes('재질')) { if (!materialKey) materialKey = fieldKey; } // 종류 필드 (type_1, type_2, type_3 등 모두 수집) - ID와 함께 저장 if ((lowerKey.includes('category') || lowerKey.includes('종류') || lowerKey.includes('type_') || fieldName === '종류' || fieldName.includes('종류')) && !lowerKey.includes('item_name') && !lowerKey.includes('item_type') && !lowerKey.includes('part_type') && !fieldName.includes('품목명')) { categoryKeysWithIds.push({ key: fieldKey, id: field.id }); } // 폭 합계 필드 if (lowerKey.includes('width_sum') || lowerKey.includes('폭합계') || lowerKey.includes('폭_합계') || lowerKey.includes('width_total') || fieldName.includes('폭 합계') || fieldName.includes('폭합계')) { if (!widthSumKey) widthSumKey = fieldKey; } // 모양&길이 필드 if (lowerKey.includes('shape_length') || lowerKey.includes('모양') || fieldName.includes('모양') || fieldName.includes('길이')) { if (!shapeLengthKey) shapeLengthKey = fieldKey; } }; // 모든 필드 검사 structure.sections.forEach((section) => { section.fields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); }); structure.directFields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); // 품목코드 자동생성 (품목명 + 종류 + 모양&길이) // itemNameKey 또는 직접 탐지한 bendingItemNameKey 사용 const effectiveItemNameKey = bendingItemNameKey || itemNameKey; const itemNameValue = effectiveItemNameKey ? (formData[effectiveItemNameKey] as string) || '' : ''; // 2025-12-04: 종류 필드 선택 - 조건부 표시로 현재 보이는 필드만 검사 // shouldShowField를 직접 호출할 수 없으므로, 값이 있는 필드 중 마지막 것을 선택 // (품목명 변경 시 이전 종류는 초기화되므로, 현재 표시되는 종류만 값이 있음) let activeCategoryKey = ''; let categoryValue = ''; for (const { key: catKey, id: catId } of categoryKeysWithIds) { const val = (formData[catKey] as string) || ''; if (val) { // 마지막으로 선택된 종류 필드를 사용 (최신 값) activeCategoryKey = catKey; categoryValue = val; // break 제거 - 마지막 값이 있는 필드 사용 } } const shapeLengthValue = shapeLengthKey ? (formData[shapeLengthKey] as string) || '' : ''; const autoCode = generateBendingItemCodeSimple(itemNameValue, categoryValue, shapeLengthValue); // console.log('[DynamicItemForm] 절곡부품 필드 탐지:', { bendingItemNameKey, materialKey, activeCategoryKey, autoCode }); return { bendingFieldKeys: { material: materialKey, category: activeCategoryKey, // 현재 활성화된 종류 필드 widthSum: widthSumKey, shapeLength: shapeLengthKey, itemName: effectiveItemNameKey, }, autoBendingItemCode: autoCode, allCategoryKeysWithIds: categoryKeysWithIds, // 모든 종류 필드 키+ID (초기화용) }; }, [structure, selectedItemType, isBendingPart, formData, itemNameKey]); // 2025-12-04: 품목명 변경 시 종류 필드 값 초기화 // 품목명(A)→종류(A1) 선택 후 품목명(B)로 변경 시, 이전 종류(A1) 값이 남아있어서 // 새 종류(B1) 선택해도 이전 값이 품목코드에 적용되는 버그 수정 const prevItemNameValueRef = useRef(''); useEffect(() => { if (!isBendingPart || !bendingFieldKeys.itemName) return; const currentItemNameValue = (formData[bendingFieldKeys.itemName] as string) || ''; const prevItemNameValue = prevItemNameValueRef.current; // 품목명이 변경되었고, 이전 값이 있었을 때만 종류 필드 초기화 if (prevItemNameValue && prevItemNameValue !== currentItemNameValue) { // console.log('[DynamicItemForm] 품목명 변경 감지:', prevItemNameValue, '→', currentItemNameValue); // 모든 종류 필드 값 초기화 allCategoryKeysWithIds.forEach(({ key }) => { const currentVal = (formData[key] as string) || ''; if (currentVal) { // console.log('[DynamicItemForm] 종류 필드 초기화:', key); setFieldValue(key, ''); } }); } // 현재 값을 이전 값으로 저장 prevItemNameValueRef.current = currentItemNameValue; }, [isBendingPart, bendingFieldKeys.itemName, formData, allCategoryKeysWithIds, setFieldValue]); // BOM 필요 체크박스 필드 키 탐지 (structure에서 직접 검색) const bomRequiredFieldKey = useMemo(() => { if (!structure) return ''; // 모든 섹션의 필드에서 BOM 관련 체크박스 필드 찾기 for (const section of structure.sections) { for (const f of section.fields) { const field = f.field; const fieldKey = field.field_key || ''; const fieldName = field.field_name || ''; const fieldType = field.field_type || ''; // 체크박스 타입이고 BOM 관련 필드인지 확인 const isCheckbox = fieldType.toLowerCase() === 'checkbox' || fieldType.toLowerCase() === 'boolean'; const isBomRelated = fieldKey.toLowerCase().includes('bom') || fieldName.toLowerCase().includes('bom') || fieldName.includes('부품구성') || fieldKey.includes('부품구성'); if (isCheckbox && isBomRelated) { // console.log('[DynamicItemForm] BOM 체크박스 필드 발견:', { fieldKey, fieldName }); return field.field_key || `field_${field.id}`; } } } // 직접 필드에서도 찾기 for (const f of structure.directFields) { const field = f.field; const fieldKey = field.field_key || ''; const fieldName = field.field_name || ''; const fieldType = field.field_type || ''; const isCheckbox = fieldType.toLowerCase() === 'checkbox' || fieldType.toLowerCase() === 'boolean'; const isBomRelated = fieldKey.toLowerCase().includes('bom') || fieldName.toLowerCase().includes('bom') || fieldName.includes('부품구성') || fieldKey.includes('부품구성'); if (isCheckbox && isBomRelated) { // console.log('[DynamicItemForm] BOM 체크박스 필드 발견 (직접필드):', { fieldKey, fieldName }); return field.field_key || `field_${field.id}`; } } // console.log('[DynamicItemForm] BOM 체크박스 필드를 찾지 못함'); return ''; }, [structure]); // 조립 부품 필드 탐지 (측면규격 가로/세로, 길이) - 자동생성용 // 2025-12-03: 필드 탐지 조건 개선 - 더 정확한 매칭 const { hasAssemblyFields, assemblyFieldKeys, autoAssemblyItemName, autoAssemblySpec } = useMemo(() => { if (!structure || selectedItemType !== 'PT') { return { hasAssemblyFields: false, assemblyFieldKeys: { sideSpecWidth: '', sideSpecHeight: '', assemblyLength: '' }, autoAssemblyItemName: '', autoAssemblySpec: '', }; } let sideSpecWidthKey = ''; let sideSpecHeightKey = ''; let assemblyLengthKey = ''; const checkField = (fieldKey: string, field: ItemFieldResponse) => { const fieldName = field.field_name || ''; const lowerKey = fieldKey.toLowerCase(); // 측면규격 가로 - 더 정확한 조건 (측면 + 가로 조합) const isWidthField = lowerKey.includes('side_spec_width') || lowerKey.includes('sidespecwidth') || fieldName.includes('측면규격(가로)') || fieldName.includes('측면 규격(가로)') || fieldName.includes('측면규격 가로') || fieldName.includes('측면 가로') || (fieldName.includes('측면') && fieldName.includes('가로')); if (isWidthField && !sideSpecWidthKey) { sideSpecWidthKey = fieldKey; } // 측면규격 세로 - 더 정확한 조건 (측면 + 세로 조합) const isHeightField = lowerKey.includes('side_spec_height') || lowerKey.includes('sidespecheight') || fieldName.includes('측면규격(세로)') || fieldName.includes('측면 규격(세로)') || fieldName.includes('측면규격 세로') || fieldName.includes('측면 세로') || (fieldName.includes('측면') && fieldName.includes('세로')); if (isHeightField && !sideSpecHeightKey) { sideSpecHeightKey = fieldKey; } // 길이 - 조립 부품 길이 필드 const isLengthField = lowerKey.includes('assembly_length') || lowerKey.includes('assemblylength') || lowerKey === 'length' || lowerKey.endsWith('_length') || fieldName === '길이' || fieldName.includes('조립') && fieldName.includes('길이'); if (isLengthField && !assemblyLengthKey) { assemblyLengthKey = fieldKey; } }; // 모든 필드 검사 structure.sections.forEach((section) => { section.fields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); }); structure.directFields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); // 조립 부품 여부: 측면규격 가로/세로, 길이 필드가 모두 있어야 함 const isAssembly = !!(sideSpecWidthKey && sideSpecHeightKey && assemblyLengthKey); // 자동생성 값 계산 const selectedItemName = itemNameKey ? (formData[itemNameKey] as string) || '' : ''; const sideSpecWidth = sideSpecWidthKey ? (formData[sideSpecWidthKey] as string) || '' : ''; const sideSpecHeight = sideSpecHeightKey ? (formData[sideSpecHeightKey] as string) || '' : ''; const assemblyLength = assemblyLengthKey ? (formData[assemblyLengthKey] as string) || '' : ''; // 품목명: 선택한 품목명 가로x세로 const autoItemName = generateAssemblyItemNameSimple(selectedItemName, sideSpecWidth, sideSpecHeight); // 규격: 가로x세로x길이(네자리) const autoSpec = generateAssemblySpecification(sideSpecWidth, sideSpecHeight, assemblyLength); // console.log('[DynamicItemForm] 조립 부품 필드 탐지:', { isAssembly, autoItemName, autoSpec }); return { hasAssemblyFields: isAssembly, assemblyFieldKeys: { sideSpecWidth: sideSpecWidthKey, sideSpecHeight: sideSpecHeightKey, assemblyLength: assemblyLengthKey, }, autoAssemblyItemName: autoItemName, autoAssemblySpec: autoSpec, }; }, [structure, selectedItemType, formData, itemNameKey]); // 구매 부품(전동개폐기) 필드 탐지 - 품목명, 용량, 전원 // 2025-12-04: 구매 부품 품목코드 자동생성 추가 const { purchasedFieldKeys, autoPurchasedItemCode } = useMemo(() => { if (!structure || selectedItemType !== 'PT' || !isPurchasedPart) { return { purchasedFieldKeys: { itemName: '', // 품목명 (전동개폐기 등) capacity: '', // 용량 (150, 300, etc.) power: '', // 전원 (220V, 380V) }, autoPurchasedItemCode: '', }; } let purchasedItemNameKey = ''; let capacityKey = ''; let powerKey = ''; const checkField = (fieldKey: string, field: ItemFieldResponse) => { const fieldName = field.field_name || ''; const lowerKey = fieldKey.toLowerCase(); // 구매 부품 품목명 필드 탐지 - PurchasedItemName 우선 탐지 const isPurchasedItemNameField = lowerKey.includes('purchaseditemname'); const isItemNameField = isPurchasedItemNameField || lowerKey.includes('item_name') || lowerKey.includes('품목명') || fieldName.includes('품목명') || fieldName === '품목명'; // PurchasedItemName을 우선적으로 사용 (더 정확한 매칭) if (isPurchasedItemNameField) { purchasedItemNameKey = fieldKey; // 덮어쓰기 (우선순위 높음) } else if (isItemNameField && !purchasedItemNameKey) { purchasedItemNameKey = fieldKey; } // 용량 필드 탐지 const isCapacityField = lowerKey.includes('capacity') || lowerKey.includes('용량') || fieldName.includes('용량') || fieldName === '용량'; if (isCapacityField && !capacityKey) { capacityKey = fieldKey; } // 전원 필드 탐지 const isPowerField = lowerKey.includes('power') || lowerKey.includes('전원') || fieldName.includes('전원') || fieldName === '전원'; if (isPowerField && !powerKey) { powerKey = fieldKey; } }; // 모든 필드 검사 structure.sections.forEach((section) => { section.fields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); }); structure.directFields.forEach((f) => { const key = f.field.field_key || `field_${f.field.id}`; checkField(key, f.field); }); // 품목코드 자동생성: 품목명 + 용량 + 전원 const itemNameValue = purchasedItemNameKey ? (formData[purchasedItemNameKey] as string) || '' : ''; const capacityValue = capacityKey ? (formData[capacityKey] as string) || '' : ''; const powerValue = powerKey ? (formData[powerKey] as string) || '' : ''; const autoCode = generatePurchasedItemCode(itemNameValue, capacityValue, powerValue); // console.log('[DynamicItemForm] 구매 부품 필드 탐지:', { purchasedItemNameKey, autoCode }); return { purchasedFieldKeys: { itemName: purchasedItemNameKey, capacity: capacityKey, power: powerKey, }, autoPurchasedItemCode: autoCode, }; }, [structure, selectedItemType, isPurchasedPart, formData]); // 품목코드 자동생성 값 // PT(부품): 영문약어-순번 (예: GR-001, MOTOR-002) // 기타 품목: 품목명-규격 (기존 방식) // 2025-12-03: 연동 드롭다운 로직 제거 - 단순화 // 2025-12-03: activeSpecificationKey 사용하여 조건부 표시 고려 const autoGeneratedItemCode = useMemo(() => { if (!hasAutoItemCode) return ''; // field_key가 "{id}_item_name" 형식일 수 있어서 동적으로 키 사용 const itemName = (formData[itemNameKey] as string) || ''; // 현재 표시 중인 규격 필드 값 사용 (조건부 표시 고려) const specification = activeSpecificationKey ? (formData[activeSpecificationKey] as string) || '' : ''; if (!itemName) return ''; // PT(부품)인 경우: 영문약어-순번 형식 사용 if (selectedItemType === 'PT') { // generateItemCode는 품목명을 기반으로 영문약어를 찾고 순번을 계산 const generatedCode = generateItemCode(itemName, existingItemCodes); return generatedCode; } // 기타 품목: 기존 방식 (품목명-규격) if (!specification) return itemName; return `${itemName}-${specification}`; }, [hasAutoItemCode, itemNameKey, activeSpecificationKey, formData, selectedItemType, existingItemCodes]); // 품목 유형 변경 핸들러 const handleItemTypeChange = (type: ItemType) => { setSelectedItemType(type); }; // 폼 제출 핸들러 const handleFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); // 밸리데이션 - 조건부 표시로 숨겨진 필드는 이미 allFields에서 제외됨 // 2025-12-03: 연동 드롭다운 로직 제거 - 단순화 const isValid = validateAll(allFields); if (!isValid) { return; } // field_key → 백엔드 필드명 매핑 // field_key 형식: "{id}_{key}" (예: "98_item_name", "110_품목명") // 백엔드 필드명으로 변환 필요 // 2025-12-03: 한글 field_key 지원 추가 const fieldKeyToBackendKey: Record = { 'item_name': 'name', 'productName': 'name', // FG(제품) 품목명 필드 '품목명': 'name', // 한글 field_key 지원 'specification': 'spec', 'standard': 'spec', // 규격 대체 필드명 '규격': 'spec', // 한글 field_key 지원 '사양': 'spec', // 한글 대체 'unit': 'unit', '단위': 'unit', // 한글 field_key 지원 'note': 'note', '비고': 'note', // 한글 field_key 지원 'description': 'description', '설명': 'description', // 한글 field_key 지원 'part_type': 'part_type', '부품유형': 'part_type', // 한글 field_key 지원 '부품 유형': 'part_type', // 공백 포함 한글 'is_active': 'is_active', 'status': 'is_active', 'active': 'is_active', '품목상태': 'is_active', // 한글 field_key 지원 '품목 상태': 'is_active', // 공백 포함 한글 '상태': 'is_active', // 짧은 한글 }; // formData를 백엔드 필드명으로 변환 console.log('========== [DynamicItemForm] 저장 시 formData =========='); console.log('specification 관련:', Object.entries(formData).filter(([k]) => k.includes('specification') || k.includes('규격'))); console.log('is_active 관련:', Object.entries(formData).filter(([k]) => k.includes('active') || k.includes('상태'))); console.log('전체 formData:', formData); console.log('========================================================='); const convertedData: Record = {}; Object.entries(formData).forEach(([key, value]) => { // "{id}_{fieldKey}" 형식 체크: 숫자로 시작하고 _가 있는 경우 // 예: "98_item_name" → true, "item_name" → false const isFieldKeyFormat = /^\d+_/.test(key); if (isFieldKeyFormat) { // "{id}_{fieldKey}" 형식에서 fieldKey 추출 const underscoreIndex = key.indexOf('_'); const fieldKey = key.substring(underscoreIndex + 1); const backendKey = fieldKeyToBackendKey[fieldKey] || fieldKey; // is_active 필드는 boolean으로 변환 if (backendKey === 'is_active') { // "활성", true, "true", "1", 1 등을 true로, 나머지는 false로 const isActive = value === true || value === 'true' || value === '1' || value === 1 || value === '활성' || value === 'active'; console.log(`[DynamicItemForm] is_active 변환: key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`); convertedData[backendKey] = isActive; } else { convertedData[backendKey] = value; } } else { // field_key 형식이 아닌 경우, 매핑 테이블에서 변환 시도 const backendKey = fieldKeyToBackendKey[key] || key; if (backendKey === 'is_active') { const isActive = value === true || value === 'true' || value === '1' || value === 1 || value === '활성' || value === 'active'; console.log(`[DynamicItemForm] is_active 변환 (non-field_key): key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`); convertedData[backendKey] = isActive; } else { convertedData[backendKey] = value; } } }); console.log('========== [DynamicItemForm] convertedData 결과 =========='); console.log('is_active:', convertedData.is_active); console.log('specification:', convertedData.spec || convertedData.specification); console.log('전체:', convertedData); console.log('==========================================================='); // 품목명 값 추출 (품목코드와 품목명 모두 필요) // 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; } // console.log('[DynamicItemForm] 품목명/규격 결정:', { finalName, finalSpec }); // 품목코드 결정 // 2025-12-04: 절곡 부품은 autoBendingItemCode 사용 // 2025-12-04: 구매 부품은 autoPurchasedItemCode 사용 let finalCode: string; 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: DynamicFormData = { ...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 데이터를 배열로 포함 bom: bomLines.map((line) => ({ child_item_code: line.childItemCode, child_item_name: line.childItemName, specification: line.specification || '', material: line.material || '', quantity: line.quantity, unit: line.unit, unit_price: line.unitPrice || 0, note: line.note || '', })), // 절곡품 전개도 데이터 (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', } : {}), }; // console.log('[DynamicItemForm] 제출 데이터:', submitData); 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 { console.log('[DynamicItemForm] 전개도 파일 업로드 시작:', bendingDiagramFile.name); await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', { bendingDetails: bendingDetails.length > 0 ? bendingDetails.map(d => ({ angle: d.angle || 0, length: d.width || 0, type: d.direction || '', })) : undefined, }); console.log('[DynamicItemForm] 전개도 파일 업로드 성공'); } catch (error) { console.error('[DynamicItemForm] 전개도 파일 업로드 실패:', error); fileUploadErrors.push('전개도 이미지'); } } // FG (제품) 시방서 업로드 if (selectedItemType === 'FG' && specificationFile) { try { console.log('[DynamicItemForm] 시방서 파일 업로드 시작:', specificationFile.name); await uploadItemFile(itemId, specificationFile, 'specification'); console.log('[DynamicItemForm] 시방서 파일 업로드 성공'); } catch (error) { console.error('[DynamicItemForm] 시방서 파일 업로드 실패:', error); fileUploadErrors.push('시방서'); } } // FG (제품) 인정서 업로드 if (selectedItemType === 'FG' && certificationFile) { try { console.log('[DynamicItemForm] 인정서 파일 업로드 시작:', certificationFile.name); // 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', { certificationNumber: certNumber, certificationStartDate: certStartDate, certificationEndDate: certEndDate, }); console.log('[DynamicItemForm] 인정서 파일 업로드 성공'); } catch (error) { console.error('[DynamicItemForm] 인정서 파일 업로드 실패:', error); fileUploadErrors.push('인정서'); } } // 파일 업로드 실패 경고 (품목은 저장됨) if (fileUploadErrors.length > 0) { console.warn('[DynamicItemForm] 일부 파일 업로드 실패:', fileUploadErrors.join(', ')); // 품목은 저장되었으므로 경고만 표시하고 진행 alert(`품목이 저장되었습니다.\n\n일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}\n수정 화면에서 다시 업로드해 주세요.`); } } router.push('/items'); router.refresh(); }); }; // 로딩 상태 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 */} {/* 헤더 */} router.back()} /> {/* 기본 정보 - 목업과 동일한 레이아웃 (모든 필드 한 줄씩) */} 기본 정보 {/* 품목 유형 선택 */}

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

{/* 직접 필드 (페이지에 직접 연결된 필드) */} {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} /> ); })} {/* 첫 번째 섹션의 필드 렌더링 */} {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} /> {/* 규격 필드 바로 다음에 품목코드 자동생성 필드 표시 (절곡부품 제외) */} {isSpecField && hasAutoItemCode && !isBendingPart && (

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

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

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

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

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

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

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

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

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

)} {/* FG(제품) 전용: 인정 유효기간 종료일 다음에 시방서/인정서 파일 업로드 */} {isCertEndDateField && selectedItemType === 'FG' && (
{/* 시방서 파일 */}
{/* 기존 파일 표시 (edit 모드) */} {mode === 'edit' && existingSpecificationFile && !specificationFile && (
{existingSpecificationFileName}
)} {/* 새 파일 업로드 */} { const file = e.target.files?.[0] || null; setSpecificationFile(file); }} disabled={isSubmitting} className="cursor-pointer" /> {specificationFile && (

선택된 파일: {specificationFile.name}

)}
{/* 인정서 파일 */}
{/* 기존 파일 표시 (edit 모드) */} {mode === 'edit' && existingCertificationFile && !certificationFile && (
{existingCertificationFileName}
)} {/* 새 파일 업로드 */} { const file = e.target.files?.[0] || null; setCertificationFile(file); }} disabled={isSubmitting} className="cursor-pointer" /> {certificationFile && (

선택된 파일: {certificationFile.name}

)}
)}
); })} {/* 추가 섹션들 (기본 정보 카드 내에 하위 섹션으로 통합) */} {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} /> ); })}
); })}
{/* 품목 유형 선택 안내 경고 */} {!selectedItemType && ( ⚠️ 품목 유형을 먼저 선택해주세요 )} {/* 조립품 전개도 섹션 (PT - 조립 부품 전용) - 품목명 선택 시 표시 */} {selectedItemType === 'PT' && isAssemblyPart && ( setFieldValue(key, value)} isSubmitting={isSubmitting} /> )} {/* 절곡품 전개도 섹션 (PT - 절곡 부품 전용) */} {selectedItemType === 'PT' && isBendingPart && ( setFieldValue(key, value)} isSubmitting={isSubmitting} /> )} {/* BOM 섹션 (부품구성 필요 체크 시에만 표시) */} {selectedItemType && bomSection && (() => { // bomRequiredFieldKey는 useMemo에서 structure 기반으로 미리 계산됨 const bomValue = bomRequiredFieldKey ? formData[bomRequiredFieldKey] : undefined; const isBomRequired = bomValue === true || bomValue === 'true' || bomValue === '1' || bomValue === 1; // 디버깅 로그 // console.log('[DynamicItemForm] BOM 체크 디버깅:', { bomRequiredFieldKey, bomValue, isBomRequired }); if (!isBomRequired) return null; return ( ); })()} {/* 전개도 그리기 다이얼로그 (절곡품/조립품 공용) */} { setBendingDiagram(imageData); setIsDrawingOpen(false); }} initialImage={bendingDiagram} title={isAssemblyPart ? "조립품 전개도" : "절곡품 전개도"} description={isAssemblyPart ? "조립품 전개도(바라시)를 그리거나 편집합니다." : "절곡품 전개도를 그리거나 편집합니다." } /> ); }