fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ export interface UseTemplateManagementReturn {
|
||||
handleUpdateSectionTemplate: () => void;
|
||||
handleDeleteSectionTemplate: (id: number) => void;
|
||||
handleLoadTemplate: (selectedPage: ItemPage | undefined) => void;
|
||||
handleAddTemplateField: () => void;
|
||||
handleAddTemplateField: () => Promise<void>;
|
||||
handleEditTemplateField: (templateId: number, field: TemplateField) => void;
|
||||
handleDeleteTemplateField: (templateId: number, fieldId: string) => void;
|
||||
handleAddBOMItemToTemplate: (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => void;
|
||||
@@ -89,7 +89,20 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
addSectionToPage,
|
||||
addItemMasterField,
|
||||
itemMasterFields,
|
||||
tenantId
|
||||
tenantId,
|
||||
// 2025-11-26: sectionsAsTemplates가 itemPages에서 파생되므로
|
||||
// 섹션 탭에서 수정/삭제 시 실제 섹션 API를 호출해야 함
|
||||
updateSection,
|
||||
deleteSection,
|
||||
itemPages,
|
||||
// 2025-11-26: 섹션 탭에서 새 섹션 추가 시 독립 섹션으로 생성
|
||||
createIndependentSection,
|
||||
// 2025-11-27: entity_relationships 기반 필드 연결/해제
|
||||
linkFieldToSection,
|
||||
unlinkFieldFromSection,
|
||||
independentFields,
|
||||
// 2025-11-27: 필드 수정 API
|
||||
updateField,
|
||||
} = useItemMaster();
|
||||
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -127,28 +140,33 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
|
||||
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
|
||||
|
||||
// 섹션 템플릿 추가
|
||||
const handleAddSectionTemplate = () => {
|
||||
// 섹션 템플릿 추가 (2025-11-26: 독립 섹션으로 생성하여 sectionsAsTemplates에 반영)
|
||||
const handleAddSectionTemplate = async () => {
|
||||
if (!newSectionTemplateTitle.trim()) {
|
||||
toast.error('섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTemplateData = {
|
||||
tenant_id: tenantId ?? 0,
|
||||
template_name: newSectionTemplateTitle,
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM',
|
||||
description: newSectionTemplateDescription || null,
|
||||
default_fields: null,
|
||||
category: newSectionTemplateCategory,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
// 2025-11-26: sectionsAsTemplates가 itemPages + independentSections에서 파생되므로
|
||||
// 독립 섹션으로 생성해야 화면에 바로 반영됨
|
||||
const newSectionData = {
|
||||
title: newSectionTemplateTitle,
|
||||
type: newSectionTemplateType as 'fields' | 'bom',
|
||||
description: newSectionTemplateDescription || undefined,
|
||||
is_template: true, // 섹션 탭에서 생성된 섹션은 템플릿으로 표시
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
console.log('Adding section template:', newTemplateData);
|
||||
addSectionTemplate(newTemplateData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션 템플릿이 추가되었습니다!');
|
||||
console.log('Adding independent section (from section tab):', newSectionData);
|
||||
|
||||
try {
|
||||
await createIndependentSection(newSectionData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션이 추가되었습니다!');
|
||||
} catch (error) {
|
||||
console.error('섹션 추가 실패:', error);
|
||||
toast.error('섹션 추가에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 섹션 템플릿 수정 시작
|
||||
@@ -161,31 +179,43 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
setIsSectionTemplateDialogOpen(true);
|
||||
};
|
||||
|
||||
// 섹션 템플릿 업데이트
|
||||
const handleUpdateSectionTemplate = () => {
|
||||
// 섹션 템플릿 업데이트 (2025-11-26: sectionsAsTemplates 사용으로 실제 섹션 API 호출)
|
||||
const handleUpdateSectionTemplate = async () => {
|
||||
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim()) {
|
||||
toast.error('섹션 제목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// sectionsAsTemplates가 itemPages에서 파생되므로, 실제 섹션을 업데이트해야 함
|
||||
const updateData = {
|
||||
template_name: newSectionTemplateTitle,
|
||||
title: newSectionTemplateTitle,
|
||||
description: newSectionTemplateDescription || undefined,
|
||||
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
};
|
||||
|
||||
console.log('Updating section template:', { id: editingSectionTemplateId, updateData });
|
||||
updateSectionTemplate(editingSectionTemplateId, updateData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션이 수정되었습니다 (저장 필요)');
|
||||
console.log('Updating section (from template handler):', { id: editingSectionTemplateId, updateData });
|
||||
try {
|
||||
// updateSection 호출 (템플릿이 아닌 실제 섹션 API)
|
||||
await updateSection(editingSectionTemplateId, updateData);
|
||||
resetSectionTemplateForm();
|
||||
toast.success('섹션이 수정되었습니다!');
|
||||
} catch (error) {
|
||||
console.error('섹션 수정 실패:', error);
|
||||
toast.error('섹션 수정에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 섹션 템플릿 삭제
|
||||
const handleDeleteSectionTemplate = (id: number) => {
|
||||
// 섹션 템플릿 삭제 (2025-11-26: sectionsAsTemplates 사용으로 실제 섹션 API 호출)
|
||||
const handleDeleteSectionTemplate = async (id: number) => {
|
||||
if (confirm('이 섹션을 삭제하시겠습니까?')) {
|
||||
deleteSectionTemplate(id);
|
||||
toast.success('섹션이 삭제되었습니다');
|
||||
try {
|
||||
// deleteSection 호출 (템플릿이 아닌 실제 섹션 API)
|
||||
await deleteSection(id);
|
||||
toast.success('섹션이 삭제되었습니다!');
|
||||
} catch (error) {
|
||||
console.error('섹션 삭제 실패:', error);
|
||||
toast.error('섹션 삭제에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,14 +234,16 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
|
||||
const newSection = {
|
||||
page_id: selectedPage.id,
|
||||
section_name: template.template_name,
|
||||
title: template.template_name,
|
||||
section_type: template.section_type === 'BOM' ? 'BOM' as const : 'BASIC' as const,
|
||||
description: template.description || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_template: false,
|
||||
is_default: false,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: [],
|
||||
bomItems: template.section_type === 'BOM' ? [] : undefined
|
||||
bom_items: template.section_type === 'BOM' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Loading template to section:', template.template_name, 'newSection:', newSection);
|
||||
@@ -221,77 +253,57 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
toast.success('섹션이 불러와졌습니다');
|
||||
};
|
||||
|
||||
// 템플릿 필드 추가
|
||||
const handleAddTemplateField = () => {
|
||||
// 템플릿 필드 추가/수정 (2025-11-27: API 사용으로 변경)
|
||||
// sectionsAsTemplates가 itemPages + independentSections에서 파생되므로
|
||||
// entity_relationships 기반 연결 API를 사용해야 실시간 반영됨
|
||||
const handleAddTemplateField = async () => {
|
||||
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === currentTemplateId);
|
||||
if (!template) return;
|
||||
try {
|
||||
// 수정 모드: 기존 필드 속성 업데이트
|
||||
if (editingTemplateFieldId) {
|
||||
const updateData = {
|
||||
field_name: templateFieldName,
|
||||
field_type: templateFieldInputType,
|
||||
is_required: templateFieldRequired,
|
||||
placeholder: templateFieldDescription || 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,
|
||||
},
|
||||
};
|
||||
|
||||
// 마스터 필드에 없으면 자동 추가
|
||||
const existingMasterField = itemMasterFields.find(f => f.id.toString() === templateFieldKey);
|
||||
if (!existingMasterField && !editingTemplateFieldId) {
|
||||
const newMasterFieldData: Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> = {
|
||||
field_name: templateFieldName,
|
||||
field_type: templateFieldInputType,
|
||||
category: '공통',
|
||||
description: templateFieldDescription || null,
|
||||
is_common: false,
|
||||
default_value: null,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
validation_rules: 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
|
||||
},
|
||||
};
|
||||
addItemMasterField(newMasterFieldData as any);
|
||||
toast.success('항목 탭에 자동으로 추가되었습니다');
|
||||
await updateField(editingTemplateFieldId, updateData);
|
||||
toast.success('항목이 수정되었습니다');
|
||||
resetTemplateFieldForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// 추가 모드: 기존 필드를 섹션에 연결
|
||||
const existingField = independentFields.find(f => f.id.toString() === templateFieldKey);
|
||||
|
||||
if (existingField) {
|
||||
await linkFieldToSection(currentTemplateId, existingField.id);
|
||||
toast.success('항목이 섹션에 연결되었습니다');
|
||||
} else {
|
||||
toast.error('항목 탭에서 먼저 항목을 생성해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
resetTemplateFieldForm();
|
||||
} catch (error) {
|
||||
console.error('항목 처리 실패:', error);
|
||||
toast.error('항목 처리에 실패했습니다');
|
||||
}
|
||||
|
||||
// TemplateField 형식으로 생성
|
||||
const newField: TemplateField = {
|
||||
id: String(editingTemplateFieldId || Date.now()),
|
||||
name: templateFieldName,
|
||||
fieldKey: templateFieldKey,
|
||||
property: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => o.trim())
|
||||
: undefined,
|
||||
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
||||
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
||||
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
|
||||
},
|
||||
description: templateFieldDescription || undefined
|
||||
};
|
||||
|
||||
let updatedFields;
|
||||
const currentFields = template.default_fields
|
||||
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
|
||||
: [];
|
||||
|
||||
if (editingTemplateFieldId) {
|
||||
updatedFields = Array.isArray(currentFields)
|
||||
? currentFields.map((f: any) => String(f.id) === String(editingTemplateFieldId) ? newField : f)
|
||||
: [];
|
||||
toast.success('항목이 수정되었습니다');
|
||||
} else {
|
||||
updatedFields = Array.isArray(currentFields) ? [...currentFields, newField] : [newField];
|
||||
toast.success('항목이 추가되었습니다');
|
||||
}
|
||||
|
||||
updateSectionTemplate(currentTemplateId, { default_fields: updatedFields });
|
||||
resetTemplateFieldForm();
|
||||
};
|
||||
|
||||
// 템플릿 필드 수정 시작
|
||||
@@ -310,21 +322,20 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
setIsTemplateFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
// 템플릿 필드 삭제
|
||||
const handleDeleteTemplateField = (templateId: number, fieldId: string) => {
|
||||
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
||||
// 템플릿 필드 연결 해제 (2025-11-27: entity_relationships 기반으로 변경)
|
||||
// sectionId = templateId (sectionsAsTemplates에서 섹션 ID로 사용)
|
||||
// fieldId = 실제 item_fields의 ID
|
||||
const handleDeleteTemplateField = async (templateId: number, fieldId: string) => {
|
||||
if (!confirm('이 항목의 연결을 해제하시겠습니까?\n(항목 자체는 삭제되지 않고 항목 탭에 유지됩니다)')) return;
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template) return;
|
||||
|
||||
const currentFields = template.default_fields
|
||||
? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields)
|
||||
: [];
|
||||
const updatedFields = Array.isArray(currentFields)
|
||||
? currentFields.filter((f: any) => String(f.id) !== String(fieldId))
|
||||
: [];
|
||||
updateSectionTemplate(templateId, { default_fields: updatedFields });
|
||||
toast.success('항목이 삭제되었습니다');
|
||||
try {
|
||||
// entity_relationships 기반 연결 해제 API 호출
|
||||
await unlinkFieldFromSection(templateId, Number(fieldId));
|
||||
toast.success('항목 연결이 해제되었습니다');
|
||||
} catch (error) {
|
||||
console.error('항목 연결 해제 실패:', error);
|
||||
toast.error('항목 연결 해제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 추가
|
||||
|
||||
Reference in New Issue
Block a user