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}`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 하드코딩 내역 목록 (문서화용)
|
||||
// ============================================
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => router.push(`/items/${encodeURIComponent(item.itemCode)}/edit`)}
|
||||
onClick={() => router.push(`/items/${encodeURIComponent(item.itemCode)}/edit?type=${item.itemType}&id=${item.id}`)}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
수정
|
||||
|
||||
@@ -109,20 +109,16 @@ export default function ProductForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<div>
|
||||
<Label>품목코드 (자동생성)</Label>
|
||||
<Input
|
||||
value={(() => {
|
||||
const pName = productName || '';
|
||||
const iName = getValues('itemName') || '';
|
||||
return pName && iName ? `${pName}-${iName}` : '';
|
||||
})()}
|
||||
value={getValues('itemName') || ''}
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
placeholder="상품명과 품목명을 입력하면 자동으로 생성됩니다"
|
||||
placeholder="품목명이 입력되면 자동으로 동일하게 생성됩니다"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 품목코드는 '상품명-품목명' 형식으로 자동 생성됩니다
|
||||
* 품목명과 품목코드가 동일하게 설정됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -159,6 +155,15 @@ export default function ProductForm({
|
||||
* 비활성 시 품목 사용이 제한됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>비고</Label>
|
||||
<Input
|
||||
placeholder="비고 사항을 입력하세요"
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -199,7 +204,7 @@ export function ProductCertificationSection({
|
||||
</div>
|
||||
|
||||
{/* 인정번호, 유효기간, 파일 업로드, 비고 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="certificationNumber">인정번호</Label>
|
||||
<Input
|
||||
@@ -232,20 +237,28 @@ export function ProductCertificationSection({
|
||||
|
||||
{/* 시방서 파일 */}
|
||||
<div className="space-y-2">
|
||||
<Label>시방서 (PDF, DOCX, HWP, JPG, PNG / 최대 20MB)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="file"
|
||||
accept=".pdf,.docx,.hwp,.jpg,.jpeg,.png"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSpecificationFile(file);
|
||||
}
|
||||
}}
|
||||
className="flex-1"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label>시방서 (PDF)</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="cursor-pointer">
|
||||
<span className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium border rounded-md bg-background hover:bg-accent hover:text-accent-foreground">
|
||||
파일 선택
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSpecificationFile(file);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{specificationFile ? specificationFile.name : '선택된 파일 없음'}
|
||||
</span>
|
||||
{specificationFile && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -253,34 +266,38 @@ export function ProductCertificationSection({
|
||||
size="sm"
|
||||
onClick={() => setSpecificationFile(null)}
|
||||
disabled={isSubmitting}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{specificationFile && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
첨부됨: {specificationFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 인정서 파일 */}
|
||||
<div className="space-y-2">
|
||||
<Label>인정서 (PDF, DOCX, HWP, JPG, PNG / 최대 20MB)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="file"
|
||||
accept=".pdf,.docx,.hwp,.jpg,.jpeg,.png"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setCertificationFile(file);
|
||||
}
|
||||
}}
|
||||
className="flex-1"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Label>인정서 (PDF)</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="cursor-pointer">
|
||||
<span className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium border rounded-md bg-background hover:bg-accent hover:text-accent-foreground">
|
||||
파일 선택
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setCertificationFile(file);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{certificationFile ? certificationFile.name : '선택된 파일 없음'}
|
||||
</span>
|
||||
{certificationFile && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -288,20 +305,16 @@ export function ProductCertificationSection({
|
||||
size="sm"
|
||||
onClick={() => setCertificationFile(null)}
|
||||
disabled={isSubmitting}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{certificationFile && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
첨부됨: {certificationFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비고 */}
|
||||
<div className="md:col-span-2">
|
||||
<div>
|
||||
<Label>비고</Label>
|
||||
<Textarea
|
||||
value={remarks}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* - 전동개폐기, 모터, 체인 등
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import type { UseFormRegister, UseFormSetValue, FieldErrors } from 'react-hook-form';
|
||||
import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
import { PART_TYPE_CATEGORIES } from '../../constants';
|
||||
import { generatePurchasedItemCode } from '@/components/items/DynamicItemForm/utils/itemCodeGenerator';
|
||||
|
||||
export interface PurchasedPartFormProps {
|
||||
selectedCategory1: string;
|
||||
@@ -60,6 +62,18 @@ export default function PurchasedPartForm({
|
||||
setValue,
|
||||
errors,
|
||||
}: PurchasedPartFormProps) {
|
||||
// 전동개폐기 품목코드 자동생성 (품목명 + 용량 + 전원)
|
||||
const generatedItemCode = useMemo(() => {
|
||||
if (selectedCategory1 === 'electric_opener') {
|
||||
const category = PART_TYPE_CATEGORIES.PURCHASED?.categories.find(
|
||||
c => c.value === selectedCategory1
|
||||
);
|
||||
const itemName = category?.label || '';
|
||||
return generatePurchasedItemCode(itemName, electricOpenerCapacity, electricOpenerPower);
|
||||
}
|
||||
return '';
|
||||
}, [selectedCategory1, electricOpenerCapacity, electricOpenerPower]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 품목명 선택 */}
|
||||
@@ -258,19 +272,21 @@ export default function PurchasedPartForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 품목코드 자동생성 */}
|
||||
<div className="md:col-span-2">
|
||||
<Label>품목코드 (자동생성)</Label>
|
||||
<Input
|
||||
value=""
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
placeholder="품목명과 규격이 입력되면 자동으로 생성됩니다"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 품목코드는 '품목명-규격' 형식으로 자동 생성됩니다
|
||||
</p>
|
||||
</div>
|
||||
{/* 품목코드 자동생성 - 전동개폐기만 표시 */}
|
||||
{selectedCategory1 === 'electric_opener' && (
|
||||
<div className="md:col-span-2">
|
||||
<Label>품목코드 (자동생성)</Label>
|
||||
<Input
|
||||
value={generatedItemCode}
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
placeholder="용량과 전원을 선택하면 자동으로 생성됩니다"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 품목코드는 '품목명+용량+전원' 형식으로 자동 생성됩니다 (예: 전동개폐기150KG380V)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 품목 상태 */}
|
||||
<div className="md:col-span-2">
|
||||
|
||||
@@ -90,7 +90,10 @@ export default function ItemListClient() {
|
||||
|
||||
// 삭제 다이얼로그 상태
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState<{ id: string; code: string } | null>(null);
|
||||
const [itemToDelete, setItemToDelete] = useState<{ id: string; code: string; itemType: string } | null>(null);
|
||||
|
||||
// Materials 타입 (SM, RM, CS는 Material 테이블 사용)
|
||||
const MATERIAL_TYPES = ['SM', 'RM', 'CS'];
|
||||
|
||||
// API에서 품목 목록 및 테이블 컬럼 조회 (서버 사이드 검색/필터링)
|
||||
const {
|
||||
@@ -148,17 +151,19 @@ export default function ItemListClient() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleView = (itemCode: string) => {
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}`);
|
||||
const handleView = (itemCode: string, itemType: string, itemId: string) => {
|
||||
// itemType을 query param으로 전달 (Materials 조회를 위해)
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}?type=${itemType}&id=${itemId}`);
|
||||
};
|
||||
|
||||
const handleEdit = (itemCode: string) => {
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}/edit`);
|
||||
const handleEdit = (itemCode: string, itemType: string, itemId: string) => {
|
||||
// itemType을 query param으로 전달 (Materials 조회를 위해)
|
||||
router.push(`/items/${encodeURIComponent(itemCode)}/edit?type=${itemType}&id=${itemId}`);
|
||||
};
|
||||
|
||||
// 삭제 확인 다이얼로그 열기
|
||||
const openDeleteDialog = (itemId: string, itemCode: string) => {
|
||||
setItemToDelete({ id: itemId, code: itemCode });
|
||||
const openDeleteDialog = (itemId: string, itemCode: string, itemType: string) => {
|
||||
setItemToDelete({ id: itemId, code: itemCode, itemType });
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -168,7 +173,17 @@ export default function ItemListClient() {
|
||||
|
||||
try {
|
||||
console.log('[Delete] 삭제 요청:', itemToDelete);
|
||||
const response = await fetch(`/api/proxy/items/${itemToDelete.id}`, {
|
||||
|
||||
// Materials (SM, RM, CS)는 /products/materials 엔드포인트 사용
|
||||
// Products (FG, PT)는 /items 엔드포인트 사용
|
||||
const isMaterial = MATERIAL_TYPES.includes(itemToDelete.itemType);
|
||||
const deleteUrl = isMaterial
|
||||
? `/api/proxy/products/materials/${itemToDelete.id}`
|
||||
: `/api/proxy/items/${itemToDelete.id}`;
|
||||
|
||||
console.log('[Delete] URL:', deleteUrl, '(isMaterial:', isMaterial, ')');
|
||||
|
||||
const response = await fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -222,7 +237,15 @@ export default function ItemListClient() {
|
||||
|
||||
for (const id of itemIds) {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy/items/${id}`, {
|
||||
// 해당 품목의 itemType 찾기
|
||||
const item = items.find((i) => i.id === id);
|
||||
const isMaterial = item ? MATERIAL_TYPES.includes(item.itemType) : false;
|
||||
// Materials는 /products/materials 엔드포인트, Products는 /items 엔드포인트
|
||||
const deleteUrl = isMaterial
|
||||
? `/api/proxy/products/materials/${id}`
|
||||
: `/api/proxy/items/${id}`;
|
||||
|
||||
const response = await fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
@@ -329,7 +352,7 @@ export default function ItemListClient() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleView(item.itemCode)}
|
||||
onClick={(e) => { e.stopPropagation(); handleView(item.itemCode, item.itemType, item.id); }}
|
||||
title="상세 보기"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
@@ -337,7 +360,7 @@ export default function ItemListClient() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(item.itemCode)}
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.itemCode, item.itemType, item.id); }}
|
||||
title="수정"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
@@ -345,7 +368,7 @@ export default function ItemListClient() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openDeleteDialog(item.id, item.itemCode)}
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item.id, item.itemCode, item.itemType); }}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
@@ -388,7 +411,7 @@ export default function ItemListClient() {
|
||||
}
|
||||
isSelected={isSelected}
|
||||
onToggleSelection={onToggle}
|
||||
onCardClick={() => handleView(item.itemCode)}
|
||||
onCardClick={() => handleView(item.itemCode, item.itemType, item.id)}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
{item.specification && (
|
||||
@@ -400,34 +423,37 @@ export default function ItemListClient() {
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<div className="flex items-center justify-end gap-1 pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleView(item.itemCode); }}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Search className="h-4 w-4 mr-1" />
|
||||
<span className="text-xs">상세</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.itemCode); }}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
<span className="text-xs">수정</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item.id, item.itemCode); }}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
isSelected ? (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleView(item.itemCode, item.itemType, item.id); }}
|
||||
>
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
상세
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(item.itemCode, item.itemType, item.id); }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-[rgba(255,255,255,0)]"
|
||||
onClick={(e) => { e.stopPropagation(); openDeleteDialog(item.id, item.itemCode, item.itemType); }}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -426,9 +426,25 @@ export function FieldDialog({
|
||||
<DialogFooter className="shrink-0 bg-white z-10 px-6 py-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>취소</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log('[FieldDialog] 🔵 저장 버튼 클릭!', {
|
||||
fieldInputMode,
|
||||
editingFieldId,
|
||||
selectedMasterFieldId,
|
||||
newFieldName,
|
||||
newFieldKey,
|
||||
isNameEmpty,
|
||||
isKeyEmpty,
|
||||
isKeyInvalid,
|
||||
});
|
||||
setIsSubmitted(true);
|
||||
// 2025-11-28: field_key validation 추가
|
||||
if ((fieldInputMode === 'custom' || editingFieldId) && (isNameEmpty || isKeyEmpty || isKeyInvalid)) return;
|
||||
const shouldValidate = fieldInputMode === 'custom' || editingFieldId;
|
||||
console.log('[FieldDialog] 🔵 shouldValidate:', shouldValidate);
|
||||
if (shouldValidate && (isNameEmpty || isKeyEmpty || isKeyInvalid)) {
|
||||
console.log('[FieldDialog] ❌ 유효성 검사 실패로 return');
|
||||
return;
|
||||
}
|
||||
console.log('[FieldDialog] ✅ handleAddField 호출');
|
||||
await handleAddField();
|
||||
setIsSubmitted(false);
|
||||
}}>저장</Button>
|
||||
|
||||
@@ -117,8 +117,8 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
|
||||
if (masterField) {
|
||||
setNewFieldName(masterField.field_name);
|
||||
// 2025-11-28: field_key 사용 (없으면 빈 문자열로 사용자가 입력하도록)
|
||||
setNewFieldKey('');
|
||||
// 2025-12-04: master 모드에서 field_key를 field_{id} 형태로 설정 (백엔드 검증 통과용)
|
||||
setNewFieldKey(`field_${selectedMasterFieldId}`);
|
||||
setNewFieldInputType(masterField.field_type || 'textbox');
|
||||
// properties에서 required 확인, 또는 validation_rules에서 확인
|
||||
const isRequired = (masterField.properties as any)?.required || false;
|
||||
@@ -139,7 +139,22 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
|
||||
// 필드 추가 (2025-11-27: async/await 추가 - 다른 탭 실시간 동기화)
|
||||
const handleAddField = async (selectedPage: ItemPage | undefined) => {
|
||||
console.log('[useFieldManagement] 🟢 handleAddField 시작!', {
|
||||
selectedPage: selectedPage?.id,
|
||||
selectedSectionForField,
|
||||
newFieldName,
|
||||
newFieldKey,
|
||||
fieldInputMode,
|
||||
selectedMasterFieldId,
|
||||
});
|
||||
|
||||
if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim()) {
|
||||
console.log('[useFieldManagement] ❌ 필수값 누락으로 return', {
|
||||
selectedPage: !!selectedPage,
|
||||
selectedSectionForField,
|
||||
newFieldName: newFieldName.trim(),
|
||||
newFieldKey: newFieldKey.trim(),
|
||||
});
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user