fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가
## 품목관리 수정 버그 수정 - FG(제품) 수정 시 품목명 반영 안되는 문제 해결 - productName → name 필드 매핑 추가 - FG 품목코드 = 품목명 동기화 로직 추가 - Materials(SM, RM, CS) 수정페이지 진입 오류 해결 - UNIQUE 제약조건 위반 오류 해결 ## Sales 페이지 - 거래처관리 (client-management-sales-admin) 페이지 구현 - 견적관리 (quote-management) 페이지 구현 - 관련 컴포넌트 및 훅 추가 ## 기타 - 회원가입 페이지 차단 처리 - 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import type {
|
||||
DynamicFormData,
|
||||
DynamicFormErrors,
|
||||
@@ -25,6 +25,21 @@ export function useDynamicFormState(
|
||||
const [errors, setErrors] = useState<DynamicFormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 2025-12-04: Edit 모드에서 initialData가 비동기로 로드될 때 formData 동기화
|
||||
// useState의 초기값은 첫 렌더 시에만 사용되므로,
|
||||
// initialData가 나중에 변경되면 formData를 업데이트해야 함
|
||||
const isInitialDataLoaded = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// initialData가 있고, 아직 로드되지 않았을 때만 동기화
|
||||
// (사용자가 수정 중인 데이터를 덮어쓰지 않도록)
|
||||
if (initialData && Object.keys(initialData).length > 0 && !isInitialDataLoaded.current) {
|
||||
console.log('[useDynamicFormState] initialData 동기화:', initialData);
|
||||
setFormData(initialData);
|
||||
isInitialDataLoaded.current = true;
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// 필드 값 설정
|
||||
const setFieldValue = useCallback((fieldKey: string, value: DynamicFieldValue) => {
|
||||
setFormData((prev) => ({
|
||||
@@ -149,17 +164,21 @@ export function useDynamicFormState(
|
||||
);
|
||||
|
||||
// 폼 제출
|
||||
// 2025-12-04: 실패 시에만 버튼 다시 활성화 (로그인 방식)
|
||||
// 성공 시에는 페이지 이동하므로 버튼 비활성화 상태 유지
|
||||
const handleSubmit = useCallback(
|
||||
async (onSubmit: (data: DynamicFormData) => Promise<void>) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await onSubmit(formData);
|
||||
// 성공 시: setIsSubmitting(false)를 호출하지 않음
|
||||
// 페이지 이동하므로 버튼 비활성화 상태 유지 → 중복 클릭 방지
|
||||
} catch (err) {
|
||||
console.error('폼 제출 실패:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
// 실패 시에만 버튼 다시 활성화 → 재시도 가능
|
||||
setIsSubmitting(false);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[formData]
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
generateAssemblyItemNameSimple,
|
||||
generateAssemblySpecification,
|
||||
generateBendingItemCodeSimple,
|
||||
generatePurchasedItemCode,
|
||||
} from './utils/itemCodeGenerator';
|
||||
import type { DynamicItemFormProps, DynamicFormData, DynamicSection, DynamicFieldValue, BOMLine, BOMSearchState } from './types';
|
||||
import type { ItemType, BendingDetail } from '@/types/item';
|
||||
@@ -255,6 +256,10 @@ export default function DynamicItemForm({
|
||||
const [bendingDetails, setBendingDetails] = useState<BendingDetail[]>([]);
|
||||
const [widthSum, setWidthSum] = useState<string>('');
|
||||
|
||||
// FG(제품) 전용 파일 업로드 상태 관리
|
||||
const [specificationFile, setSpecificationFile] = useState<File | null>(null);
|
||||
const [certificationFile, setCertificationFile] = useState<File | null>(null);
|
||||
|
||||
// 조건부 표시 관리
|
||||
const { shouldShowSection, shouldShowField } = useConditionalDisplay(structure, formData);
|
||||
|
||||
@@ -274,7 +279,7 @@ export default function DynamicItemForm({
|
||||
.map((item: { code?: string; item_code?: string }) => item.code || item.item_code || '')
|
||||
.filter((code: string) => code);
|
||||
setExistingItemCodes(codes);
|
||||
console.log('[DynamicItemForm] PT 기존 품목코드 로드:', codes.length, '개');
|
||||
// console.log('[DynamicItemForm] PT 기존 품목코드 로드:', codes.length, '개');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[DynamicItemForm] PT 품목코드 조회 실패:', err);
|
||||
@@ -287,7 +292,7 @@ export default function DynamicItemForm({
|
||||
}
|
||||
}, [selectedItemType]);
|
||||
|
||||
// 품목 유형 변경 시 폼 초기화
|
||||
// 품목 유형 변경 시 폼 초기화 (create 모드)
|
||||
useEffect(() => {
|
||||
if (selectedItemType && mode === 'create' && structure) {
|
||||
// 기본값 설정
|
||||
@@ -322,6 +327,82 @@ 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)가 다른 문제 해결
|
||||
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
||||
|
||||
// console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
|
||||
|
||||
// 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 매핑
|
||||
|
||||
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;
|
||||
}
|
||||
// 매핑 없는 경우 그대로 유지
|
||||
else {
|
||||
mappedData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log('[DynamicItemForm] Mapped initialData:', mappedData);
|
||||
|
||||
// 변환된 데이터로 폼 리셋
|
||||
resetForm(mappedData);
|
||||
setIsEditDataMapped(true);
|
||||
}, [mode, structure, initialData, isEditDataMapped, resetForm]);
|
||||
|
||||
// 모든 필드 목록 (밸리데이션용) - 숨겨진 섹션/필드 제외
|
||||
const allFields = useMemo<ItemFieldResponse[]>(() => {
|
||||
if (!structure) return [];
|
||||
@@ -440,10 +521,10 @@ export default function DynamicItemForm({
|
||||
return allSpecificationKeys[0] || '';
|
||||
}, [structure, allSpecificationKeys, shouldShowSection, shouldShowField]);
|
||||
|
||||
// 부품 유형 필드 탐지 (PT 품목에서 절곡/조립 부품 판별용)
|
||||
const { partTypeFieldKey, selectedPartType, isBendingPart, isAssemblyPart } = useMemo(() => {
|
||||
// 부품 유형 필드 탐지 (PT 품목에서 절곡/조립/구매 부품 판별용)
|
||||
const { partTypeFieldKey, selectedPartType, isBendingPart, isAssemblyPart, isPurchasedPart } = useMemo(() => {
|
||||
if (!structure || selectedItemType !== 'PT') {
|
||||
return { partTypeFieldKey: '', selectedPartType: '', isBendingPart: false, isAssemblyPart: false };
|
||||
return { partTypeFieldKey: '', selectedPartType: '', isBendingPart: false, isAssemblyPart: false, isPurchasedPart: false };
|
||||
}
|
||||
|
||||
let foundPartTypeKey = '';
|
||||
@@ -477,19 +558,17 @@ export default function DynamicItemForm({
|
||||
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,
|
||||
});
|
||||
// console.log('[DynamicItemForm] 부품 유형 감지:', { partTypeFieldKey: foundPartTypeKey, currentPartType, isBending, isAssembly, isPurchased });
|
||||
|
||||
return {
|
||||
partTypeFieldKey: foundPartTypeKey,
|
||||
selectedPartType: currentPartType,
|
||||
isBendingPart: isBending,
|
||||
isAssemblyPart: isAssembly,
|
||||
isPurchasedPart: isPurchased,
|
||||
};
|
||||
}, [structure, selectedItemType, formData]);
|
||||
|
||||
@@ -508,7 +587,7 @@ export default function DynamicItemForm({
|
||||
|
||||
// 이전 값이 있고, 현재 값과 다른 경우에만 초기화
|
||||
if (prevPartType && prevPartType !== currentPartType) {
|
||||
console.log('[DynamicItemForm] 부품 유형 변경 감지:', prevPartType, '→', currentPartType);
|
||||
// console.log('[DynamicItemForm] 부품 유형 변경 감지:', prevPartType, '→', currentPartType);
|
||||
|
||||
// setTimeout으로 다음 틱에서 초기화 실행
|
||||
// → 부품 유형 Select 값 변경이 먼저 완료된 후 초기화
|
||||
@@ -555,7 +634,7 @@ export default function DynamicItemForm({
|
||||
|
||||
// 중복 제거 후 초기화
|
||||
const uniqueFields = [...new Set(fieldsToReset)];
|
||||
console.log('[DynamicItemForm] 초기화할 필드:', uniqueFields);
|
||||
// console.log('[DynamicItemForm] 초기화할 필드:', uniqueFields);
|
||||
|
||||
uniqueFields.forEach((fieldKey) => {
|
||||
setFieldValue(fieldKey, '');
|
||||
@@ -612,12 +691,12 @@ export default function DynamicItemForm({
|
||||
|
||||
// bending_parts는 무조건 우선 (덮어쓰기)
|
||||
if (isBendingItemNameField) {
|
||||
console.log('[checkField] 절곡부품 품목명 필드 발견!', { fieldKey, fieldName });
|
||||
// console.log('[checkField] 절곡부품 품목명 필드 발견!', { fieldKey, fieldName });
|
||||
bendingItemNameKey = fieldKey;
|
||||
}
|
||||
// 일반 품목명은 아직 없을 때만
|
||||
else if (isGeneralItemNameField && !bendingItemNameKey) {
|
||||
console.log('[checkField] 일반 품목명 필드 발견!', { fieldKey, fieldName });
|
||||
// console.log('[checkField] 일반 품목명 필드 발견!', { fieldKey, fieldName });
|
||||
bendingItemNameKey = fieldKey;
|
||||
}
|
||||
|
||||
@@ -686,19 +765,7 @@ export default function DynamicItemForm({
|
||||
|
||||
const autoCode = generateBendingItemCodeSimple(itemNameValue, categoryValue, shapeLengthValue);
|
||||
|
||||
console.log('[DynamicItemForm] 절곡부품 필드 탐지:', {
|
||||
bendingItemNameKey,
|
||||
itemNameKey,
|
||||
effectiveItemNameKey,
|
||||
materialKey,
|
||||
categoryKeysWithIds,
|
||||
activeCategoryKey,
|
||||
widthSumKey,
|
||||
shapeLengthKey,
|
||||
formDataKeys: Object.keys(formData),
|
||||
values: { itemNameValue, categoryValue, shapeLengthValue },
|
||||
autoCode,
|
||||
});
|
||||
// console.log('[DynamicItemForm] 절곡부품 필드 탐지:', { bendingItemNameKey, materialKey, activeCategoryKey, autoCode });
|
||||
|
||||
return {
|
||||
bendingFieldKeys: {
|
||||
@@ -726,13 +793,13 @@ export default function DynamicItemForm({
|
||||
|
||||
// 품목명이 변경되었고, 이전 값이 있었을 때만 종류 필드 초기화
|
||||
if (prevItemNameValue && prevItemNameValue !== currentItemNameValue) {
|
||||
console.log('[DynamicItemForm] 품목명 변경 감지:', prevItemNameValue, '→', currentItemNameValue);
|
||||
// console.log('[DynamicItemForm] 품목명 변경 감지:', prevItemNameValue, '→', currentItemNameValue);
|
||||
|
||||
// 모든 종류 필드 값 초기화
|
||||
allCategoryKeysWithIds.forEach(({ key }) => {
|
||||
const currentVal = (formData[key] as string) || '';
|
||||
if (currentVal) {
|
||||
console.log('[DynamicItemForm] 종류 필드 초기화:', key);
|
||||
// console.log('[DynamicItemForm] 종류 필드 초기화:', key);
|
||||
setFieldValue(key, '');
|
||||
}
|
||||
});
|
||||
@@ -763,12 +830,7 @@ export default function DynamicItemForm({
|
||||
fieldKey.includes('부품구성');
|
||||
|
||||
if (isCheckbox && isBomRelated) {
|
||||
console.log('[DynamicItemForm] BOM 체크박스 필드 발견:', {
|
||||
fieldKey,
|
||||
fieldName,
|
||||
fieldType,
|
||||
resultKey: field.field_key || `field_${field.id}`,
|
||||
});
|
||||
// console.log('[DynamicItemForm] BOM 체크박스 필드 발견:', { fieldKey, fieldName });
|
||||
return field.field_key || `field_${field.id}`;
|
||||
}
|
||||
}
|
||||
@@ -789,17 +851,12 @@ export default function DynamicItemForm({
|
||||
fieldKey.includes('부품구성');
|
||||
|
||||
if (isCheckbox && isBomRelated) {
|
||||
console.log('[DynamicItemForm] BOM 체크박스 필드 발견 (직접필드):', {
|
||||
fieldKey,
|
||||
fieldName,
|
||||
fieldType,
|
||||
resultKey: field.field_key || `field_${field.id}`,
|
||||
});
|
||||
// console.log('[DynamicItemForm] BOM 체크박스 필드 발견 (직접필드):', { fieldKey, fieldName });
|
||||
return field.field_key || `field_${field.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DynamicItemForm] BOM 체크박스 필드를 찾지 못함');
|
||||
// console.log('[DynamicItemForm] BOM 체크박스 필드를 찾지 못함');
|
||||
return '';
|
||||
}, [structure]);
|
||||
|
||||
@@ -878,15 +935,7 @@ export default function DynamicItemForm({
|
||||
// 규격: 가로x세로x길이(네자리)
|
||||
const autoSpec = generateAssemblySpecification(sideSpecWidth, sideSpecHeight, assemblyLength);
|
||||
|
||||
console.log('[DynamicItemForm] 조립 부품 필드 탐지:', {
|
||||
isAssembly,
|
||||
sideSpecWidthKey,
|
||||
sideSpecHeightKey,
|
||||
assemblyLengthKey,
|
||||
values: { sideSpecWidth, sideSpecHeight, assemblyLength },
|
||||
autoItemName,
|
||||
autoSpec,
|
||||
});
|
||||
// console.log('[DynamicItemForm] 조립 부품 필드 탐지:', { isAssembly, autoItemName, autoSpec });
|
||||
|
||||
return {
|
||||
hasAssemblyFields: isAssembly,
|
||||
@@ -900,6 +949,97 @@ export default function DynamicItemForm({
|
||||
};
|
||||
}, [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)
|
||||
// 기타 품목: 품목명-규격 (기존 방식)
|
||||
@@ -949,6 +1089,7 @@ export default function DynamicItemForm({
|
||||
// 2025-12-03: 한글 field_key 지원 추가
|
||||
const fieldKeyToBackendKey: Record<string, string> = {
|
||||
'item_name': 'name',
|
||||
'productName': 'name', // FG(제품) 품목명 필드
|
||||
'품목명': 'name', // 한글 field_key 지원
|
||||
'specification': 'spec',
|
||||
'standard': 'spec', // 규격 대체 필드명
|
||||
@@ -972,11 +1113,16 @@ export default function DynamicItemForm({
|
||||
};
|
||||
|
||||
// formData를 백엔드 필드명으로 변환
|
||||
// console.log('[DynamicItemForm] formData before conversion:', formData);
|
||||
const convertedData: Record<string, any> = {};
|
||||
Object.entries(formData).forEach(([key, value]) => {
|
||||
// "{id}_{fieldKey}" 형식에서 fieldKey 추출
|
||||
const underscoreIndex = key.indexOf('_');
|
||||
if (underscoreIndex > 0) {
|
||||
// "{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;
|
||||
|
||||
@@ -990,10 +1136,19 @@ export default function DynamicItemForm({
|
||||
convertedData[backendKey] = value;
|
||||
}
|
||||
} else {
|
||||
// 변환 불필요한 필드는 그대로
|
||||
convertedData[key] = value;
|
||||
// field_key 형식이 아닌 경우, 매핑 테이블에서 변환 시도
|
||||
const backendKey = fieldKeyToBackendKey[key] || key;
|
||||
|
||||
if (backendKey === 'is_active') {
|
||||
const isActive = value === true || value === 'true' || value === '1' ||
|
||||
value === 1 || value === '활성' || value === 'active';
|
||||
convertedData[backendKey] = isActive;
|
||||
} else {
|
||||
convertedData[backendKey] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
// console.log('[DynamicItemForm] convertedData after conversion:', convertedData);
|
||||
|
||||
// 품목명 값 추출 (품목코드와 품목명 모두 필요)
|
||||
// 2025-12-04: 절곡 부품은 별도 품목명 필드(bendingFieldKeys.itemName) 사용
|
||||
@@ -1004,9 +1159,10 @@ export default function DynamicItemForm({
|
||||
? (formData[effectiveItemNameKeyForSubmit] as string) || ''
|
||||
: '';
|
||||
|
||||
// 조립/절곡 부품 자동생성 값 결정
|
||||
// 조립/절곡/구매 부품 자동생성 값 결정
|
||||
// 조립 부품: 품목명 = "품목명 가로x세로", 규격 = "가로x세로x길이"
|
||||
// 절곡 부품: 품목명 = bendingFieldKeys.itemName에서 선택한 값, 규격 = 없음 (품목코드로 대체)
|
||||
// 구매 부품: 품목명 = purchasedFieldKeys.itemName에서 선택한 값
|
||||
let finalName: string;
|
||||
let finalSpec: string | undefined;
|
||||
|
||||
@@ -1018,27 +1174,29 @@ export default function DynamicItemForm({
|
||||
// 절곡 부품: 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] 품목명/규격 결정:', {
|
||||
isAssemblyPart,
|
||||
autoAssemblyItemName,
|
||||
autoAssemblySpec,
|
||||
convertedDataName: convertedData.name,
|
||||
convertedDataSpec: convertedData.spec,
|
||||
finalName,
|
||||
finalSpec,
|
||||
});
|
||||
// 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 {
|
||||
@@ -1078,16 +1236,17 @@ export default function DynamicItemForm({
|
||||
part_type: 'ASSEMBLY',
|
||||
bending_diagram: bendingDiagram || null, // 조립품도 동일한 전개도 필드 사용
|
||||
} : {}),
|
||||
// 구매품 데이터 (PT - 구매 부품 전용)
|
||||
...(selectedItemType === 'PT' && isPurchasedPart ? {
|
||||
part_type: 'PURCHASED',
|
||||
} : {}),
|
||||
// FG(제품)은 단위 필드가 없으므로 기본값 'EA' 설정
|
||||
...(selectedItemType === 'FG' && !convertedData.unit ? {
|
||||
unit: 'EA',
|
||||
} : {}),
|
||||
};
|
||||
|
||||
// is_active 디버깅 로그
|
||||
console.log('[DynamicItemForm] is_active 디버깅:', {
|
||||
formDataKeys: Object.keys(formData).filter(k => k.includes('active') || k.includes('상태') || k.includes('status')),
|
||||
convertedIsActive: convertedData.is_active,
|
||||
submitDataIsActive: submitData.is_active,
|
||||
formDataValues: Object.entries(formData).filter(([k]) => k.includes('active') || k.includes('상태') || k.includes('status')),
|
||||
});
|
||||
console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
||||
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
||||
|
||||
await handleSubmit(async () => {
|
||||
await onSubmit(submitData);
|
||||
@@ -1211,10 +1370,17 @@ export default function DynamicItemForm({
|
||||
|
||||
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 && (
|
||||
@@ -1283,6 +1449,87 @@ export default function DynamicItemForm({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* 비고 필드 다음에 구매부품(전동개폐기) 품목코드 자동생성 */}
|
||||
{isNoteField && isPurchasedPart && (
|
||||
<div className="mt-4">
|
||||
<Label htmlFor="purchased_item_code_auto">품목코드 (자동생성)</Label>
|
||||
<Input
|
||||
id="purchased_item_code_auto"
|
||||
value={autoPurchasedItemCode || ''}
|
||||
placeholder="품목명, 용량, 전원을 선택하면 자동으로 생성됩니다"
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 품목코드는 '품목명+용량+전원' 형식으로 자동 생성됩니다 (예: 전동개폐기150KG380V)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* FG(제품) 전용: 품목명 필드 다음에 품목코드 자동생성 */}
|
||||
{isItemNameField && selectedItemType === 'FG' && (
|
||||
<div className="mt-4">
|
||||
<Label htmlFor="fg_item_code_auto">품목코드 (자동생성)</Label>
|
||||
<Input
|
||||
id="fg_item_code_auto"
|
||||
value={(formData[itemNameKey] as string) || ''}
|
||||
placeholder="품목명이 입력되면 자동으로 동일하게 생성됩니다"
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 제품(FG)의 품목코드는 품목명과 동일하게 설정됩니다
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* FG(제품) 전용: 인정 유효기간 종료일 다음에 시방서/인정서 파일 업로드 */}
|
||||
{isCertEndDateField && selectedItemType === 'FG' && (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* 시방서 파일 업로드 */}
|
||||
<div>
|
||||
<Label htmlFor="specification_file">시방서 (PDF)</Label>
|
||||
<div className="mt-1.5">
|
||||
<Input
|
||||
id="specification_file"
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0] || null;
|
||||
setSpecificationFile(file);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{specificationFile && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
선택된 파일: {specificationFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 인정서 파일 업로드 */}
|
||||
<div>
|
||||
<Label htmlFor="certification_file">인정서 (PDF)</Label>
|
||||
<div className="mt-1.5">
|
||||
<Input
|
||||
id="certification_file"
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0] || null;
|
||||
setCertificationFile(file);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{certificationFile && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
선택된 파일: {certificationFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1402,12 +1649,7 @@ export default function DynamicItemForm({
|
||||
const isBomRequired = bomValue === true || bomValue === 'true' || bomValue === '1' || bomValue === 1;
|
||||
|
||||
// 디버깅 로그
|
||||
console.log('[DynamicItemForm] BOM 체크 디버깅:', {
|
||||
bomRequiredFieldKey,
|
||||
bomValue,
|
||||
isBomRequired,
|
||||
formDataKeys: Object.keys(formData),
|
||||
});
|
||||
// console.log('[DynamicItemForm] BOM 체크 디버깅:', { bomRequiredFieldKey, bomValue, isBomRequired });
|
||||
|
||||
if (!isBomRequired) return null;
|
||||
|
||||
|
||||
@@ -355,6 +355,41 @@ export function generateAssemblySpecification(
|
||||
return `${sideSpecWidth}x${sideSpecHeight}x${assemblyLength}`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 구매 부품 (전동개폐기) 품목코드 자동생성
|
||||
// 2025-12-04 추가
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 전동개폐기 품목코드 생성 (품목명 + 용량 + 전원)
|
||||
* @param itemName 품목명 (예: "전동개폐기")
|
||||
* @param capacity 용량 (예: "150", "300")
|
||||
* @param power 전원 (예: "220V", "380V")
|
||||
* @returns 품목코드 (예: "전동개폐기150KG380V")
|
||||
*/
|
||||
export function generatePurchasedItemCode(
|
||||
itemName: string,
|
||||
capacity?: string,
|
||||
power?: string
|
||||
): string {
|
||||
if (!itemName) return '';
|
||||
|
||||
// 품목명에서 괄호 앞부분만 추출 (예: "전동개폐기 (E)" → "전동개폐기")
|
||||
const cleanItemName = itemName.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
||||
|
||||
if (!capacity || !power) {
|
||||
return cleanItemName;
|
||||
}
|
||||
|
||||
// 용량에서 'KG' 제외하고 숫자만 추출 (이미 "100KG" 형태로 들어올 수 있음)
|
||||
const cleanCapacity = capacity.replace(/KG$/i, '');
|
||||
|
||||
// 전원에서 'V' 제외하고 숫자만 추출 후 다시 V 붙이기 (일관성 유지)
|
||||
const cleanPower = power.replace(/V$/i, '') + 'V';
|
||||
|
||||
return `${cleanItemName}${cleanCapacity}KG${cleanPower}`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 하드코딩 내역 목록 (문서화용)
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user