/**
* 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 (
);
}