|
|
|
|
@@ -398,127 +398,38 @@ export default function DynamicItemForm({
|
|
|
|
|
}
|
|
|
|
|
}, [selectedItemType, structure, mode, resetForm]);
|
|
|
|
|
|
|
|
|
|
// Edit 모드: structure 로드 후 initialData를 field_key 형식으로 변환
|
|
|
|
|
// 2025-12-04: initialData 키(item_name)와 structure의 field_key(98_item_name)가 다른 문제 해결
|
|
|
|
|
// Edit 모드: initialData를 폼에 직접 로드
|
|
|
|
|
// 2025-12-09: field_key 통일로 복잡한 매핑 로직 제거
|
|
|
|
|
// 백엔드에서 field_key 그대로 응답하므로 직접 사용 가능
|
|
|
|
|
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode !== 'edit' || !structure || !initialData) return;
|
|
|
|
|
console.log('[DynamicItemForm] Edit useEffect 체크:', {
|
|
|
|
|
mode,
|
|
|
|
|
hasStructure: !!structure,
|
|
|
|
|
hasInitialData: !!initialData,
|
|
|
|
|
isEditDataMapped,
|
|
|
|
|
structureSections: structure?.sections?.length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 이미 매핑된 데이터가 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;
|
|
|
|
|
}
|
|
|
|
|
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
|
|
|
|
|
|
|
|
|
console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
|
|
|
|
|
console.log('[DynamicItemForm] Edit mode: initialData 직접 로드 (field_key 통일됨)');
|
|
|
|
|
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<string, string> = {}; // 간단한 키 → field_key 매핑
|
|
|
|
|
|
|
|
|
|
// 영문 → 한글 필드명 별칭 (API 응답 키 → structure field_name 매핑)
|
|
|
|
|
// API는 영문 키(unit, note)로 응답하지만, structure field_key는 한글(단위, 비고) 포함
|
|
|
|
|
const fieldAliases: Record<string, string> = {
|
|
|
|
|
'unit': '단위',
|
|
|
|
|
'note': '비고',
|
|
|
|
|
'remarks': '비고', // Material 모델은 remarks 사용
|
|
|
|
|
'item_name': '품목명',
|
|
|
|
|
'specification': '규격',
|
|
|
|
|
'description': '설명',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// structure의 field_key들 확인
|
|
|
|
|
const fieldKeys: string[] = [];
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
fieldKeys.push(f.field.field_key || `field_${f.field.id}`);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
console.log('[DynamicItemForm] structure field_keys:', fieldKeys);
|
|
|
|
|
console.log('[DynamicItemForm] initialData keys:', Object.keys(initialData));
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
// field_key가 통일되었으므로 initialData를 그대로 사용
|
|
|
|
|
// 기존 레거시 데이터(98_unit 형식)도 그대로 동작
|
|
|
|
|
resetForm(initialData);
|
|
|
|
|
setIsEditDataMapped(true);
|
|
|
|
|
}, [mode, structure, initialData, isEditDataMapped, resetForm]);
|
|
|
|
|
|
|
|
|
|
@@ -1202,82 +1113,22 @@ export default function DynamicItemForm({
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// field_key → 백엔드 필드명 매핑
|
|
|
|
|
// field_key 형식: "{id}_{key}" (예: "98_item_name", "110_품목명")
|
|
|
|
|
// 백엔드 필드명으로 변환 필요
|
|
|
|
|
// 2025-12-03: 한글 field_key 지원 추가
|
|
|
|
|
const fieldKeyToBackendKey: Record<string, string> = {
|
|
|
|
|
'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', // 짧은 한글
|
|
|
|
|
};
|
|
|
|
|
// 2025-12-09: field_key 통일로 변환 로직 제거
|
|
|
|
|
// formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용
|
|
|
|
|
console.log('[DynamicItemForm] 저장 시 formData:', formData);
|
|
|
|
|
|
|
|
|
|
// 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('=========================================================');
|
|
|
|
|
// is_active 필드만 boolean 변환 (드롭다운 값 → boolean)
|
|
|
|
|
const convertedData: Record<string, any> = {};
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
if (key === 'is_active' || key.endsWith('_is_active')) {
|
|
|
|
|
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false로
|
|
|
|
|
const isActive = value === true || value === 'true' || value === '1' ||
|
|
|
|
|
value === 1 || value === '활성' || value === 'active';
|
|
|
|
|
convertedData[key] = isActive;
|
|
|
|
|
} else {
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
convertedData[key] = 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) 사용
|
|
|
|
|
@@ -1333,7 +1184,8 @@ export default function DynamicItemForm({
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 품목 유형 및 BOM 데이터 추가
|
|
|
|
|
const submitData: DynamicFormData = {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
const submitData = {
|
|
|
|
|
...convertedData,
|
|
|
|
|
// 백엔드 필드명 사용
|
|
|
|
|
product_type: selectedItemType, // item_type → product_type
|
|
|
|
|
@@ -1373,7 +1225,7 @@ export default function DynamicItemForm({
|
|
|
|
|
...(selectedItemType === 'FG' && !convertedData.unit ? {
|
|
|
|
|
unit: 'EA',
|
|
|
|
|
} : {}),
|
|
|
|
|
};
|
|
|
|
|
} as DynamicFormData;
|
|
|
|
|
|
|
|
|
|
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
|
|
|
|
|
|
|
|
|
@@ -1392,9 +1244,9 @@ export default function DynamicItemForm({
|
|
|
|
|
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 || '',
|
|
|
|
|
angle: d.aAngle || 0,
|
|
|
|
|
length: d.input || 0,
|
|
|
|
|
type: d.shaded ? 'shaded' : 'normal',
|
|
|
|
|
})) : undefined,
|
|
|
|
|
});
|
|
|
|
|
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
|
|
|
|
|
@@ -1924,6 +1776,25 @@ export default function DynamicItemForm({
|
|
|
|
|
onOpenChange={setIsDrawingOpen}
|
|
|
|
|
onSave={(imageData) => {
|
|
|
|
|
setBendingDiagram(imageData);
|
|
|
|
|
|
|
|
|
|
// Base64 string을 File 객체로 변환 (업로드용)
|
|
|
|
|
// 2025-12-06: 드로잉 방식에서도 파일 업로드 지원
|
|
|
|
|
try {
|
|
|
|
|
const byteString = atob(imageData.split(',')[1]);
|
|
|
|
|
const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0];
|
|
|
|
|
const arrayBuffer = new ArrayBuffer(byteString.length);
|
|
|
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
|
for (let i = 0; i < byteString.length; i++) {
|
|
|
|
|
uint8Array[i] = byteString.charCodeAt(i);
|
|
|
|
|
}
|
|
|
|
|
const blob = new Blob([uint8Array], { type: mimeType });
|
|
|
|
|
const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType });
|
|
|
|
|
setBendingDiagramFile(file);
|
|
|
|
|
console.log('[DynamicItemForm] 드로잉 캔버스 → File 변환 성공:', file.name);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsDrawingOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
initialImage={bendingDiagram}
|
|
|
|
|
|