fix: TypeScript 타입 오류 수정 및 설정 페이지 추가

- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext)
- HeadersInit → Record<string, string> 타입 변경
- Zustand useShallow 마이그레이션 (zustand/react/shallow)
- DataTable, ListPageTemplate 제네릭 타입 제약 추가
- 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한)
- HR 관리 페이지 추가 (급여, 휴가)
- 단가관리 페이지 리팩토링

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-09 18:07:47 +09:00
parent 48dbba0e5f
commit ded0bc2439
98 changed files with 10608 additions and 1204 deletions

View File

@@ -80,22 +80,6 @@ export function DropdownField({
// 옵션이 없으면 드롭다운을 disabled로 표시
const hasOptions = options.length > 0;
// 디버깅: 단위 필드 값 추적
if (isUnitField) {
console.log('[DropdownField] 단위 필드 디버깅:', {
fieldKey,
fieldName: field.field_name,
rawValue: value,
stringValue,
isUnitField,
unitOptionsCount: unitOptions?.length || 0,
unitOptions: unitOptions?.slice(0, 3), // 처음 3개만
optionsCount: options.length,
options: options.slice(0, 3), // 처음 3개만
valueInOptions: options.some(o => o.value === stringValue),
});
}
return (
<div>
<Label htmlFor={fieldKey}>

View File

@@ -72,17 +72,6 @@ export function useConditionalDisplay(
}
});
// 디버깅: 조건부 표시 설정 확인
console.log('[useConditionalDisplay] 트리거 필드 목록:', triggers.map(t => ({
fieldKey: t.fieldKey,
fieldId: t.fieldId,
fieldConditions: t.condition.fieldConditions?.map(fc => ({
expectedValue: fc.expectedValue,
targetFieldIds: fc.targetFieldIds,
targetSectionIds: fc.targetSectionIds,
})),
})));
return triggers;
}, [structure]);
@@ -102,15 +91,6 @@ export function useConditionalDisplay(
// 현재 값과 기대값이 일치하는지 확인
const isMatch = String(currentValue) === fc.expectedValue;
// 디버깅: 조건 매칭 확인
console.log('[useConditionalDisplay] 조건 매칭 체크:', {
triggerFieldKey: trigger.fieldKey,
currentValue: String(currentValue),
expectedValue: fc.expectedValue,
isMatch,
targetFieldIds: fc.targetFieldIds,
});
if (isMatch) {
// 일치하면 타겟 섹션/필드 활성화
if (fc.targetSectionIds) {
@@ -124,8 +104,6 @@ export function useConditionalDisplay(
}
});
console.log('[useConditionalDisplay] 활성화된 필드 ID:', [...activeFieldIds]);
return { activeSectionIds, activeFieldIds };
}, [triggerFields, formData]);

View File

@@ -178,7 +178,6 @@ export function useDynamicFormState(
// 폼 초기화
const resetForm = useCallback((newInitialData?: DynamicFormData) => {
console.log('[useDynamicFormState] resetForm 호출됨:', newInitialData);
setFormData(newInitialData || {});
setErrors({});
setIsSubmitting(false);

View File

@@ -398,127 +398,38 @@ export default function DynamicItemForm({
}
}, [selectedItemType, structure, mode, resetForm]);
// Edit 모드: structure 로드 후 initialData를 field_key 형식으로 변환
// 2025-12-04: initialData 키(item_name)와 structure의 field_key(98_item_name)가 다른 문제 해결
// Edit 모드: initialData를 폼에 직접 로드
// 2025-12-09: field_key 통일로 복잡한 매핑 로직 제거
// 백엔드에서 field_key 그대로 응답하므로 직접 사용 가능
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
useEffect(() => {
if (mode !== 'edit' || !structure || !initialData) return;
console.log('[DynamicItemForm] Edit useEffect 체크:', {
mode,
hasStructure: !!structure,
hasInitialData: !!initialData,
isEditDataMapped,
structureSections: structure?.sections?.length,
});
// 이미 매핑된 데이터가 formData에 있으면 스킵 (98_unit 같은 field_key 형식)
// StrictMode 리렌더에서도 안전하게 동작
const hasFieldKeyData = Object.keys(formData).some(key => /^\d+_/.test(key));
if (hasFieldKeyData) {
console.log('[DynamicItemForm] Edit mode: 이미 field_key 형식 데이터 있음, 매핑 스킵');
return;
}
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
console.log('[DynamicItemForm] Edit mode: initialData 직접 로드 (field_key 통일됨)');
console.log('[DynamicItemForm] initialData:', initialData);
// initialData의 간단한 키를 structure의 field_key로 매핑
// 예: { item_name: '테스트' } → { '98_item_name': '테스트' }
const mappedData: DynamicFormData = {};
// field_key에서 실제 필드명 추출하는 함수
// 예: '98_item_name' → 'item_name', '110_품목명' → '품목명'
const extractFieldName = (fieldKey: string): string => {
const underscoreIndex = fieldKey.indexOf('_');
if (underscoreIndex > 0) {
return fieldKey.substring(underscoreIndex + 1);
}
return fieldKey;
};
// structure에서 모든 필드의 field_key 수집
const fieldKeyMap: Record<string, string> = {}; // 간단한 키 → field_key 매핑
// 영문 → 한글 필드명 별칭 (API 응답 키 → structure field_name 매핑)
// API는 영문 키(unit, note)로 응답하지만, structure field_key는 한글(단위, 비고) 포함
const fieldAliases: Record<string, string> = {
'unit': '단위',
'note': '비고',
'remarks': '비고', // Material 모델은 remarks 사용
'item_name': '품목명',
'specification': '규격',
'description': '설명',
};
// structure의 field_key들 확인
const fieldKeys: string[] = [];
structure.sections.forEach((section) => {
section.fields.forEach((f) => {
const field = f.field;
const fieldKey = field.field_key || `field_${field.id}`;
const simpleName = extractFieldName(fieldKey);
fieldKeyMap[simpleName] = fieldKey;
// field_name도 매핑에 추가 (한글 필드명 지원)
if (field.field_name) {
fieldKeyMap[field.field_name] = fieldKey;
}
fieldKeys.push(f.field.field_key || `field_${f.field.id}`);
});
});
console.log('[DynamicItemForm] structure field_keys:', fieldKeys);
console.log('[DynamicItemForm] initialData keys:', Object.keys(initialData));
structure.directFields.forEach((f) => {
const field = f.field;
const fieldKey = field.field_key || `field_${field.id}`;
const simpleName = extractFieldName(fieldKey);
fieldKeyMap[simpleName] = fieldKey;
if (field.field_name) {
fieldKeyMap[field.field_name] = fieldKey;
}
});
console.log('[DynamicItemForm] fieldKeyMap:', fieldKeyMap);
// initialData를 field_key 형식으로 변환
Object.entries(initialData).forEach(([key, value]) => {
// 이미 field_key 형식인 경우 그대로 사용
if (key.includes('_') && /^\d+_/.test(key)) {
mappedData[key] = value;
}
// 간단한 키인 경우 field_key로 변환
else if (fieldKeyMap[key]) {
mappedData[fieldKeyMap[key]] = value;
}
// 영문 → 한글 별칭으로 시도 (API 응답 키 → structure field_name)
else if (fieldAliases[key] && fieldKeyMap[fieldAliases[key]]) {
mappedData[fieldKeyMap[fieldAliases[key]]] = value;
console.log(`[DynamicItemForm] 별칭 매핑: ${key}${fieldAliases[key]}${fieldKeyMap[fieldAliases[key]]}`);
}
// 매핑 없는 경우 그대로 유지
else {
mappedData[key] = value;
}
});
// 추가: 폼 구조의 모든 필드를 순회하면서, initialData에서 해당 값 직접 찾아서 설정
// (fieldKeyMap에 매핑이 없는 경우를 위한 fallback)
Object.entries(fieldKeyMap).forEach(([simpleName, fieldKey]) => {
// 아직 매핑 안된 필드인데 initialData에 값이 있으면 설정
if (mappedData[fieldKey] === undefined && initialData[simpleName] !== undefined) {
mappedData[fieldKey] = initialData[simpleName];
}
});
// 추가: 영문 별칭을 역으로 검색하여 매핑 (한글 field_name → 영문 API 키)
// 예: fieldKeyMap에 '단위'가 있고, initialData에 'unit'이 있으면 매핑
Object.entries(fieldAliases).forEach(([englishKey, koreanKey]) => {
const targetFieldKey = fieldKeyMap[koreanKey];
if (targetFieldKey && mappedData[targetFieldKey] === undefined && initialData[englishKey] !== undefined) {
mappedData[targetFieldKey] = initialData[englishKey];
console.log(`[DynamicItemForm] 별칭 fallback 매핑: ${englishKey}${koreanKey}${targetFieldKey}`);
}
});
console.log('========== [DynamicItemForm] Edit 모드 데이터 매핑 ==========');
console.log('specification 관련 키:', Object.keys(mappedData).filter(k => k.includes('specification') || k.includes('규격')));
console.log('is_active 관련 키:', Object.keys(mappedData).filter(k => k.includes('active') || k.includes('상태')));
console.log('매핑된 데이터:', mappedData);
console.log('==============================================================');
// 변환된 데이터로 폼 리셋
resetForm(mappedData);
// field_key가 통일되었으므로 initialData를 그대로 사용
// 기존 레거시 데이터(98_unit 형식)도 그대로 동작
resetForm(initialData);
setIsEditDataMapped(true);
}, [mode, structure, initialData, isEditDataMapped, resetForm]);
@@ -1202,82 +1113,22 @@ export default function DynamicItemForm({
return;
}
// field_key → 백엔드 필드명 매핑
// field_key 형식: "{id}_{key}" (예: "98_item_name", "110_품목명")
// 백엔드 필드명으로 변환 필요
// 2025-12-03: 한글 field_key 지원 추가
const fieldKeyToBackendKey: Record<string, string> = {
'item_name': 'name',
'productName': 'name', // FG(제품) 품목명 필드
'품목명': 'name', // 한글 field_key 지원
'specification': 'spec',
'standard': 'spec', // 규격 대체 필드명
'규격': 'spec', // 한글 field_key 지원
'사양': 'spec', // 한글 대체
'unit': 'unit',
'단위': 'unit', // 한글 field_key 지원
'note': 'note',
'비고': 'note', // 한글 field_key 지원
'description': 'description',
'설명': 'description', // 한글 field_key 지원
'part_type': 'part_type',
'부품유형': 'part_type', // 한글 field_key 지원
'부품 유형': 'part_type', // 공백 포함 한글
'is_active': 'is_active',
'status': 'is_active',
'active': 'is_active',
'품목상태': 'is_active', // 한글 field_key 지원
'품목 상태': 'is_active', // 공백 포함 한글
'상태': 'is_active', // 짧은 한글
};
// 2025-12-09: field_key 통일로 변환 로직 제거
// formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용
console.log('[DynamicItemForm] 저장 시 formData:', formData);
// formData를 백엔드 필드명으로 변환
console.log('========== [DynamicItemForm] 저장 시 formData ==========');
console.log('specification 관련:', Object.entries(formData).filter(([k]) => k.includes('specification') || k.includes('규격')));
console.log('is_active 관련:', Object.entries(formData).filter(([k]) => k.includes('active') || k.includes('상태')));
console.log('전체 formData:', formData);
console.log('=========================================================');
// is_active 필드만 boolean 변환 (드롭다운 값 → boolean)
const convertedData: Record<string, any> = {};
Object.entries(formData).forEach(([key, value]) => {
// "{id}_{fieldKey}" 형식 체크: 숫자로 시작하고 _가 있는 경우
// 예: "98_item_name" → true, "item_name" → false
const isFieldKeyFormat = /^\d+_/.test(key);
if (isFieldKeyFormat) {
// "{id}_{fieldKey}" 형식에서 fieldKey 추출
const underscoreIndex = key.indexOf('_');
const fieldKey = key.substring(underscoreIndex + 1);
const backendKey = fieldKeyToBackendKey[fieldKey] || fieldKey;
// is_active 필드는 boolean으로 변환
if (backendKey === 'is_active') {
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false로
const isActive = value === true || value === 'true' || value === '1' ||
value === 1 || value === '활성' || value === 'active';
console.log(`[DynamicItemForm] is_active 변환: key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`);
convertedData[backendKey] = isActive;
} else {
convertedData[backendKey] = value;
}
if (key === 'is_active' || key.endsWith('_is_active')) {
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false
const isActive = value === true || value === 'true' || value === '1' ||
value === 1 || value === '활성' || value === 'active';
convertedData[key] = isActive;
} else {
// field_key 형식이 아닌 경우, 매핑 테이블에서 변환 시도
const backendKey = fieldKeyToBackendKey[key] || key;
if (backendKey === 'is_active') {
const isActive = value === true || value === 'true' || value === '1' ||
value === 1 || value === '활성' || value === 'active';
console.log(`[DynamicItemForm] is_active 변환 (non-field_key): key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`);
convertedData[backendKey] = isActive;
} else {
convertedData[backendKey] = value;
}
convertedData[key] = value;
}
});
console.log('========== [DynamicItemForm] convertedData 결과 ==========');
console.log('is_active:', convertedData.is_active);
console.log('specification:', convertedData.spec || convertedData.specification);
console.log('전체:', convertedData);
console.log('===========================================================');
// 품목명 값 추출 (품목코드와 품목명 모두 필요)
// 2025-12-04: 절곡 부품은 별도 품목명 필드(bendingFieldKeys.itemName) 사용
@@ -1333,7 +1184,8 @@ export default function DynamicItemForm({
}
// 품목 유형 및 BOM 데이터 추가
const submitData: DynamicFormData = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const submitData = {
...convertedData,
// 백엔드 필드명 사용
product_type: selectedItemType, // item_type → product_type
@@ -1373,7 +1225,7 @@ export default function DynamicItemForm({
...(selectedItemType === 'FG' && !convertedData.unit ? {
unit: 'EA',
} : {}),
};
} as DynamicFormData;
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
@@ -1392,9 +1244,9 @@ export default function DynamicItemForm({
console.log('[DynamicItemForm] 전개도 파일 업로드 시작:', bendingDiagramFile.name);
await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', {
bendingDetails: bendingDetails.length > 0 ? bendingDetails.map(d => ({
angle: d.angle || 0,
length: d.width || 0,
type: d.direction || '',
angle: d.aAngle || 0,
length: d.input || 0,
type: d.shaded ? 'shaded' : 'normal',
})) : undefined,
});
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
@@ -1924,6 +1776,25 @@ export default function DynamicItemForm({
onOpenChange={setIsDrawingOpen}
onSave={(imageData) => {
setBendingDiagram(imageData);
// Base64 string을 File 객체로 변환 (업로드용)
// 2025-12-06: 드로잉 방식에서도 파일 업로드 지원
try {
const byteString = atob(imageData.split(',')[1]);
const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0];
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
const blob = new Blob([uint8Array], { type: mimeType });
const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType });
setBendingDiagramFile(file);
console.log('[DynamicItemForm] 드로잉 캔버스 → File 변환 성공:', file.name);
} catch (error) {
console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error);
}
setIsDrawingOpen(false);
}}
initialImage={bendingDiagram}

View File

@@ -118,7 +118,7 @@ export interface BOMSearchState {
/**
* 동적 폼 필드 값 타입
*/
export type DynamicFieldValue = string | number | boolean | null | undefined | Record<string, unknown>[] | Record<string, unknown>;
export type DynamicFieldValue = string | number | boolean | string[] | null | undefined | Record<string, unknown>[] | Record<string, unknown>;
/**
* 동적 폼 데이터 (field_key를 key로 사용)