refactor: 품목기준관리 서비스 레이어 도입 및 버그 수정
서비스 레이어 리팩토링: - services/ 폴더 생성 (fieldService, masterFieldService, sectionService, pageService, templateService, attributeService) - 도메인 로직 중앙화 (validation, parsing, transform) - hooks와 dialogs에서 서비스 호출로 변경 버그 수정: - 섹션탭 실시간 동기화 문제 수정 (sectionsAsTemplates 중복 제거 순서 변경) - 422 Validation Error 수정 (createIndependentField → addFieldToSection) - 페이지 삭제 시 섹션-필드 연결 유지 (refreshIndependentSections 대신 직접 이동) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { templateService } from '../services';
|
||||
|
||||
export interface UseTemplateManagementReturn {
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -103,6 +104,12 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
independentFields,
|
||||
// 2025-11-27: 필드 수정 API
|
||||
updateField,
|
||||
// 2025-12-01: 섹션에 필드 추가 API (계층구조 탭과 동일한 방식)
|
||||
addFieldToSection,
|
||||
// 2025-12-01: BOM 관리 API (API 기반으로 변경)
|
||||
addBOMItem,
|
||||
updateBOMItem,
|
||||
deleteBOMItem,
|
||||
} = useItemMaster();
|
||||
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -256,6 +263,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
// 템플릿 필드 추가/수정 (2025-11-27: API 사용으로 변경)
|
||||
// sectionsAsTemplates가 itemPages + independentSections에서 파생되므로
|
||||
// entity_relationships 기반 연결 API를 사용해야 실시간 반영됨
|
||||
// 2025-12-01: custom/master 모드 분기 처리 추가
|
||||
const handleAddTemplateField = async () => {
|
||||
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
@@ -290,15 +298,51 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
return;
|
||||
}
|
||||
|
||||
// 추가 모드: 기존 필드를 섹션에 연결
|
||||
const existingField = independentFields.find(f => f.id.toString() === templateFieldKey);
|
||||
// 추가 모드: custom/master 모드에 따라 분기 처리
|
||||
// 2025-12-01: custom 모드에서는 새 필드 생성 후 연결, master 모드에서는 기존 필드 연결
|
||||
if (templateFieldInputMode === 'master') {
|
||||
// master 모드: 기존 필드를 섹션에 연결
|
||||
// templateFieldKey는 선택된 필드의 ID (handleSelectMasterField에서 설정됨)
|
||||
const existingField = independentFields.find(f => f.id.toString() === templateFieldKey);
|
||||
|
||||
if (existingField) {
|
||||
await linkFieldToSection(currentTemplateId, existingField.id);
|
||||
toast.success('항목이 섹션에 연결되었습니다');
|
||||
if (existingField) {
|
||||
await linkFieldToSection(currentTemplateId, existingField.id);
|
||||
toast.success('항목이 섹션에 연결되었습니다');
|
||||
} else {
|
||||
toast.error('항목 탭에서 먼저 항목을 생성해주세요');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
toast.error('항목 탭에서 먼저 항목을 생성해주세요');
|
||||
return;
|
||||
// custom 모드: 섹션에 직접 필드 추가 (계층구조 탭과 동일한 방식)
|
||||
// 2025-12-01: createIndependentField + linkFieldToSection 대신 addFieldToSection 사용
|
||||
// POST /sections/{id}/fields API를 사용하여 섹션에 바로 필드 생성
|
||||
const newFieldData = {
|
||||
section_id: currentTemplateId,
|
||||
master_field_id: null,
|
||||
field_name: templateFieldName,
|
||||
field_key: templateFieldKey,
|
||||
field_type: templateFieldInputType,
|
||||
order_no: 0,
|
||||
is_required: templateFieldRequired,
|
||||
placeholder: templateFieldDescription || null,
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: null,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
properties: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
||||
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
||||
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// 섹션에 필드 추가 (계층구조 탭과 동일)
|
||||
await addFieldToSection(currentTemplateId, newFieldData);
|
||||
toast.success('항목이 섹션에 추가되었습니다');
|
||||
}
|
||||
|
||||
resetTemplateFieldForm();
|
||||
@@ -314,12 +358,8 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
setCurrentTemplateId(templateId);
|
||||
setEditingTemplateFieldId(Number(field.id));
|
||||
setTemplateFieldName(field.name);
|
||||
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||
const fieldKeyValue = field.fieldKey || '';
|
||||
const userInputPart = fieldKeyValue.includes('_')
|
||||
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||
: fieldKeyValue;
|
||||
setTemplateFieldKey(userInputPart);
|
||||
// 2025-12-01: templateService 사용으로 변경
|
||||
setTemplateFieldKey(templateService.extractUserInputFromFieldKey(field.fieldKey || ''));
|
||||
setTemplateFieldInputType(field.property.inputType);
|
||||
setTemplateFieldRequired(field.property.required);
|
||||
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
||||
@@ -346,42 +386,41 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 추가
|
||||
const handleAddBOMItemToTemplate = (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
||||
const newItem: BOMItem = {
|
||||
...item,
|
||||
id: Date.now(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tenant_id: tenantId ?? 0,
|
||||
section_id: 0
|
||||
};
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template) return;
|
||||
|
||||
const updatedBomItems = [...(template.bomItems || []), newItem];
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 추가 (2025-12-01: API 기반으로 변경)
|
||||
// templateId = sectionId (sectionsAsTemplates에서 섹션 ID로 사용)
|
||||
const handleAddBOMItemToTemplate = async (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
||||
try {
|
||||
// addBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await addBOMItem(templateId, item);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 추가 실패:', error);
|
||||
toast.error('BOM 항목 추가에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 수정
|
||||
const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.map(bom =>
|
||||
bom.id === itemId ? { ...bom, ...item } : bom
|
||||
);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 수정 (2025-12-01: API 기반으로 변경)
|
||||
const handleUpdateBOMItemInTemplate = async (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
||||
try {
|
||||
// updateBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await updateBOMItem(itemId, item);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 수정 실패:', error);
|
||||
toast.error('BOM 항목 수정에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 삭제
|
||||
const handleDeleteBOMItemFromTemplate = (templateId: number, itemId: number) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 삭제 (2025-12-01: API 기반으로 변경)
|
||||
const handleDeleteBOMItemFromTemplate = async (templateId: number, itemId: number) => {
|
||||
try {
|
||||
// deleteBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await deleteBOMItem(itemId);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 삭제 실패:', error);
|
||||
toast.error('BOM 항목 삭제에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 섹션 템플릿 폼 초기화
|
||||
|
||||
Reference in New Issue
Block a user