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:
byeongcheolryu
2025-12-04 20:52:42 +09:00
parent 42f80e2b16
commit 751e65f59b
52 changed files with 8869 additions and 1088 deletions

View File

@@ -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]

View File

@@ -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">
* &apos;++&apos; (: 전동개폐기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;

View File

@@ -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}`;
}
// ============================================
// 하드코딩 내역 목록 (문서화용)
// ============================================