feat: API 프록시 추가 및 품목기준관리 기능 개선
- HttpOnly 쿠키 기반 API 프록시 라우트 추가 (/api/proxy/[...path]) - 품목기준관리 컴포넌트 개선 (섹션, 필드, 다이얼로그) - ItemMasterContext API 연동 강화 - mock-data 제거 및 실제 API 연동 - 문서 명명규칙 정리 ([TYPE-DATE] 형식) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemSection, ItemField, FieldDisplayCondition, ItemMasterField, ItemFieldProperty, SectionTemplate, BOMItem } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemSection, ItemField, FieldDisplayCondition, ItemMasterField, ItemFieldProperty, SectionTemplate, BOMItem, TemplateField } from '@/contexts/ItemMasterContext';
|
||||
import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs';
|
||||
import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog';
|
||||
import { type ConditionalFieldConfig } from './ItemMasterDataManagement/components/ConditionalDisplayUI';
|
||||
@@ -82,6 +82,7 @@ const INPUT_TYPE_OPTIONS = [
|
||||
export function ItemMasterDataManagement() {
|
||||
const {
|
||||
itemPages,
|
||||
loadItemPages,
|
||||
addItemPage,
|
||||
updateItemPage,
|
||||
deleteItemPage,
|
||||
@@ -93,14 +94,17 @@ export function ItemMasterDataManagement() {
|
||||
deleteField,
|
||||
reorderFields,
|
||||
itemMasterFields,
|
||||
loadItemMasterFields,
|
||||
addItemMasterField,
|
||||
updateItemMasterField,
|
||||
deleteItemMasterField,
|
||||
sectionTemplates,
|
||||
loadSectionTemplates,
|
||||
addSectionTemplate,
|
||||
updateSectionTemplate,
|
||||
deleteSectionTemplate,
|
||||
resetAllData
|
||||
resetAllData,
|
||||
tenantId
|
||||
} = useItemMaster();
|
||||
|
||||
console.log('ItemMasterDataManagement: Current sectionTemplates', sectionTemplates);
|
||||
@@ -134,23 +138,17 @@ export function ItemMasterDataManagement() {
|
||||
|
||||
const data = await itemMasterApi.init();
|
||||
|
||||
// 페이지 데이터 로드 (context의 addItemPage 사용)
|
||||
data.pages.forEach(page => {
|
||||
const transformed = transformPagesResponse([page])[0];
|
||||
addItemPage(transformed);
|
||||
});
|
||||
// 페이지 데이터 로드 (이미 존재하는 데이터를 state에 로드 - API 호출 없음)
|
||||
const transformedPages = transformPagesResponse(data.pages);
|
||||
loadItemPages(transformedPages);
|
||||
|
||||
// 섹션 템플릿 로드
|
||||
data.sectionTemplates.forEach(template => {
|
||||
const transformed = transformSectionTemplatesResponse([template])[0];
|
||||
addSectionTemplate(transformed);
|
||||
});
|
||||
// 섹션 템플릿 로드 (덮어쓰기 - API 호출 없음!)
|
||||
const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates);
|
||||
loadSectionTemplates(transformedTemplates);
|
||||
|
||||
// 마스터 필드 로드
|
||||
data.masterFields.forEach(field => {
|
||||
const transformed = transformMasterFieldsResponse([field])[0];
|
||||
addItemMasterField(transformed);
|
||||
});
|
||||
// 마스터 필드 로드 (덮어쓰기 - API 호출 없음!)
|
||||
const transformedFields = transformMasterFieldsResponse(data.masterFields);
|
||||
loadItemMasterFields(transformedFields);
|
||||
|
||||
// 커스텀 탭 로드 (local state)
|
||||
if (data.customTabs && data.customTabs.length > 0) {
|
||||
@@ -207,12 +205,8 @@ export function ItemMasterDataManagement() {
|
||||
|
||||
const [activeTab, setActiveTab] = useState('hierarchy');
|
||||
|
||||
// 속성 하위 탭 관리
|
||||
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<{id: string; label: string; key: string; isDefault: boolean; order: number}>>([
|
||||
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
|
||||
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
|
||||
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
|
||||
]);
|
||||
// 속성 하위 탭 관리 (API에서 로드)
|
||||
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<{id: string; label: string; key: string; isDefault: boolean; order: number}>>([]);
|
||||
|
||||
// 마스터 항목이 추가/수정될 때 속성 탭 자동 생성
|
||||
useEffect(() => {
|
||||
@@ -344,7 +338,9 @@ export function ItemMasterDataManagement() {
|
||||
const [newSectionTitle, setNewSectionTitle] = useState('');
|
||||
const [newSectionDescription, setNewSectionDescription] = useState('');
|
||||
const [newSectionType, setNewSectionType] = useState<'fields' | 'bom'>('fields');
|
||||
|
||||
const [sectionInputMode, setSectionInputMode] = useState<'custom' | 'template'>('custom');
|
||||
const [selectedSectionTemplateId, setSelectedSectionTemplateId] = useState<number | null>(null);
|
||||
|
||||
// 모바일 체크
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
useEffect(() => {
|
||||
@@ -426,6 +422,10 @@ export function ItemMasterDataManagement() {
|
||||
const [templateFieldMultiColumn, setTemplateFieldMultiColumn] = useState(false);
|
||||
const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2);
|
||||
const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
|
||||
// 템플릿 필드 마스터 항목 관련 상태
|
||||
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom');
|
||||
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
|
||||
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
|
||||
|
||||
// BOM 관리 상태
|
||||
const [_bomItems, setBomItems] = useState<BOMItem[]>([]);
|
||||
@@ -433,6 +433,8 @@ export function ItemMasterDataManagement() {
|
||||
// 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트
|
||||
useEffect(() => {
|
||||
itemMasterFields.forEach(field => {
|
||||
// default_properties가 null/undefined인 경우 스킵
|
||||
if (!field.default_properties) return;
|
||||
const attributeType = (field.default_properties as any).attributeType;
|
||||
if (attributeType && attributeType !== 'custom' && field.default_properties?.inputType === 'dropdown') {
|
||||
let newOptions: string[] = [];
|
||||
@@ -548,20 +550,19 @@ export function ItemMasterDataManagement() {
|
||||
setIsLoading(true);
|
||||
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
|
||||
|
||||
// API 호출
|
||||
const response = await itemMasterApi.pages.create({
|
||||
// Context의 addItemPage 사용 (API 호출 + state 업데이트)
|
||||
// ⚠️ 이전 코드는 여기서 API 호출 후 addItemPage도 호출해서 API가 2번 호출되는 버그가 있었음
|
||||
const newPage = await addItemPage({
|
||||
page_name: newPageName,
|
||||
item_type: newPageItemType,
|
||||
absolute_path: absolutePath,
|
||||
is_active: true,
|
||||
sections: [],
|
||||
order_no: 0,
|
||||
});
|
||||
|
||||
// 응답 변환 및 context에 추가
|
||||
const transformedPage = transformPageResponse(response);
|
||||
addItemPage(transformedPage);
|
||||
|
||||
// 새로 생성된 페이지를 선택
|
||||
setSelectedPageId(transformedPage.id);
|
||||
setSelectedPageId(newPage.id);
|
||||
|
||||
// 폼 초기화
|
||||
setNewPageName('');
|
||||
@@ -586,43 +587,39 @@ export function ItemMasterDataManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicatePage = (pageId: number) => {
|
||||
const handleDuplicatePage = async (pageId: number) => {
|
||||
const originalPage = itemPages.find(p => p.id === pageId);
|
||||
if (!originalPage) return toast.error('페이지를 찾을 수 없습니다');
|
||||
|
||||
// 섹션 인스턴스 깊은 복사 (새로운 ID 부여)
|
||||
const duplicatedSections = originalPage.sections.map(section => ({
|
||||
...section,
|
||||
id: Date.now(),
|
||||
fields: section.fields?.map(field => ({
|
||||
...field,
|
||||
id: Date.now()
|
||||
})) || [],
|
||||
bomItems: section.bomItems?.map(item => ({
|
||||
...item,
|
||||
id: Date.now()
|
||||
}))
|
||||
}));
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 페이지 복제
|
||||
const duplicatedPageName = `${originalPage.page_name} (복제)`;
|
||||
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
|
||||
const newPage: ItemPage = {
|
||||
id: Date.now(),
|
||||
page_name: duplicatedPageName,
|
||||
item_type: originalPage.item_type,
|
||||
sections: duplicatedSections,
|
||||
is_active: true,
|
||||
absolute_path: absolutePath,
|
||||
order_no: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
// 페이지 복제
|
||||
const duplicatedPageName = `${originalPage.page_name} (복제)`;
|
||||
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
|
||||
|
||||
// 페이지 추가
|
||||
addItemPage(newPage);
|
||||
setSelectedPageId(newPage.id);
|
||||
toast.success('페이지가 복제되었습니다 (저장 필요)');
|
||||
// Context의 addItemPage 사용 (API 호출 + state 업데이트)
|
||||
const newPage = await addItemPage({
|
||||
page_name: duplicatedPageName,
|
||||
item_type: originalPage.item_type,
|
||||
sections: [], // 섹션은 별도 API로 복제해야 함
|
||||
is_active: true,
|
||||
absolute_path: absolutePath,
|
||||
order_no: 0,
|
||||
});
|
||||
|
||||
// 서버에서 반환된 ID로 선택
|
||||
setSelectedPageId(newPage.id);
|
||||
toast.success('페이지가 복제되었습니다');
|
||||
|
||||
// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
toast.error(errorMessage);
|
||||
console.error('❌ Failed to duplicate page:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSection = () => {
|
||||
@@ -657,20 +654,21 @@ export function ItemMasterDataManagement() {
|
||||
// 섹션은 페이지의 일부이므로 sections로 별도 추적하지 않음
|
||||
|
||||
// 2. 섹션관리 탭에도 템플릿으로 자동 추가 (계층구조 섹션 = 섹션 탭 섹션)
|
||||
const newTemplate: SectionTemplate = {
|
||||
id: Date.now(),
|
||||
// 프론트엔드 형식: template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
||||
const newTemplateData = {
|
||||
tenant_id: tenantId ?? 0,
|
||||
template_name: newSection.section_name,
|
||||
section_type: newSection.section_type,
|
||||
description: newSection.description,
|
||||
section_type: newSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM',
|
||||
description: newSection.description ?? null,
|
||||
default_fields: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
};
|
||||
addSectionTemplate(newTemplate);
|
||||
addSectionTemplate(newTemplateData);
|
||||
|
||||
console.log('Section added to both page and template:', {
|
||||
sectionId: newSection.id,
|
||||
templateId: newTemplate.id
|
||||
templateTitle: newTemplateData.title
|
||||
});
|
||||
|
||||
setNewSectionTitle('');
|
||||
@@ -680,6 +678,67 @@ export function ItemMasterDataManagement() {
|
||||
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`);
|
||||
};
|
||||
|
||||
// 섹션 템플릿을 페이지에 연결 (SectionDialog에서 사용)
|
||||
const handleLinkTemplate = (template: SectionTemplate) => {
|
||||
if (!selectedPage) {
|
||||
toast.error('페이지를 먼저 선택해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// 템플릿을 섹션으로 변환하여 페이지에 추가
|
||||
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
|
||||
page_id: selectedPage.id,
|
||||
section_name: template.template_name,
|
||||
section_type: template.section_type,
|
||||
description: template.description || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: template.fields ? template.fields.map((field, idx) => ({
|
||||
id: Date.now() + idx,
|
||||
section_id: 0, // 추후 업데이트됨
|
||||
field_name: field.name,
|
||||
field_type: field.property.inputType,
|
||||
order_no: idx + 1,
|
||||
is_required: field.property.required,
|
||||
placeholder: field.description || null,
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: null,
|
||||
options: field.property.options
|
||||
? field.property.options.map(opt => ({ label: opt, value: opt }))
|
||||
: null,
|
||||
properties: field.property.multiColumn ? {
|
||||
multiColumn: true,
|
||||
columnCount: field.property.columnCount,
|
||||
columnNames: field.property.columnNames
|
||||
} : null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})) : [],
|
||||
bomItems: template.section_type === 'BOM' ? (template.bomItems || []) : undefined
|
||||
};
|
||||
|
||||
console.log('Linking template to page:', {
|
||||
templateId: template.id,
|
||||
templateName: template.template_name,
|
||||
pageId: selectedPage.id,
|
||||
newSection
|
||||
});
|
||||
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
|
||||
// 다이얼로그 상태 초기화
|
||||
setSectionInputMode('custom');
|
||||
setSelectedSectionTemplateId(null);
|
||||
setNewSectionTitle('');
|
||||
setNewSectionDescription('');
|
||||
setNewSectionType('fields');
|
||||
setIsSectionDialogOpen(false);
|
||||
|
||||
toast.success(`"${template.template_name}" 템플릿이 페이지에 연결되었습니다!`);
|
||||
};
|
||||
|
||||
const handleEditSectionTitle = (sectionId: string, currentTitle: string) => {
|
||||
setEditingSectionId(sectionId);
|
||||
setEditingSectionTitle(currentTitle);
|
||||
@@ -758,9 +817,15 @@ export function ItemMasterDataManagement() {
|
||||
// 텍스트박스 컬럼 설정
|
||||
const hasColumns = newFieldInputType === 'textbox' && textboxColumns.length > 0;
|
||||
|
||||
// 마스터 항목에서 가져온 경우 master_field_id 설정
|
||||
const masterFieldId = fieldInputMode === 'master' && selectedMasterFieldId
|
||||
? Number(selectedMasterFieldId)
|
||||
: null;
|
||||
|
||||
const newField: ItemField = {
|
||||
id: editingFieldId ? Number(editingFieldId) : Date.now(),
|
||||
section_id: Number(selectedSectionForField),
|
||||
master_field_id: masterFieldId, // 마스터 항목 연결 정보
|
||||
field_name: newFieldName,
|
||||
field_type: newFieldInputType,
|
||||
order_no: 0,
|
||||
@@ -805,17 +870,16 @@ export function ItemMasterDataManagement() {
|
||||
// 1. 섹션에 항목 추가
|
||||
addFieldToSection(Number(selectedSectionForField), newField);
|
||||
|
||||
// 2. 항목관리 탭에도 마스터 항목으로 자동 추가 (중복 체크)
|
||||
// 2. 마스터 항목 선택이 아닌 경우에만 새 마스터 항목 자동 생성
|
||||
// (마스터 항목 선택 시에는 이미 master_field_id로 연결되어 있음)
|
||||
const isFromMasterField = masterFieldId !== null;
|
||||
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
|
||||
if (!existingMasterField) {
|
||||
if (!isFromMasterField && !existingMasterField) {
|
||||
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
||||
const newMasterField: ItemMasterField = {
|
||||
id: Date.now(),
|
||||
field_name: newField.field_name,
|
||||
field_type: newField.field_type === 'textbox' ? 'TEXT' :
|
||||
newField.field_type === 'number' ? 'NUMBER' :
|
||||
newField.field_type === 'date' ? 'DATE' :
|
||||
newField.field_type === 'textarea' ? 'TEXTAREA' :
|
||||
newField.field_type === 'checkbox' ? 'CHECKBOX' : 'SELECT',
|
||||
field_type: newField.field_type, // API 스펙에 맞게 소문자 그대로 전달
|
||||
description: newField.placeholder,
|
||||
default_properties: newField.properties,
|
||||
category: selectedPage.item_type, // 현재 페이지의 품목유형을 카테고리로 설정
|
||||
@@ -975,14 +1039,11 @@ export function ItemMasterDataManagement() {
|
||||
}));
|
||||
}
|
||||
|
||||
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
||||
const newMasterField: ItemMasterField = {
|
||||
id: Date.now(),
|
||||
field_name: newMasterFieldName,
|
||||
field_type: newMasterFieldInputType === 'textbox' ? 'TEXT' :
|
||||
newMasterFieldInputType === 'number' ? 'NUMBER' :
|
||||
newMasterFieldInputType === 'date' ? 'DATE' :
|
||||
newMasterFieldInputType === 'textarea' ? 'TEXTAREA' :
|
||||
newMasterFieldInputType === 'checkbox' ? 'CHECKBOX' : 'SELECT',
|
||||
field_type: newMasterFieldInputType,
|
||||
category: newMasterFieldCategory || null,
|
||||
description: newMasterFieldDescription || null,
|
||||
default_validation: null,
|
||||
@@ -1214,21 +1275,24 @@ export function ItemMasterDataManagement() {
|
||||
|
||||
// 섹션 템플릿 핸들러
|
||||
const handleAddSectionTemplate = () => {
|
||||
if (!newSectionTemplateTitle.trim())
|
||||
if (!newSectionTemplateTitle.trim())
|
||||
return toast.error('섹션 제목을 입력해주세요');
|
||||
|
||||
const newTemplate: SectionTemplate = {
|
||||
id: Date.now(),
|
||||
// Context의 addSectionTemplate이 기대하는 SectionTemplate 형식 사용
|
||||
// template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
||||
const newTemplateData = {
|
||||
tenant_id: tenantId ?? 0,
|
||||
template_name: newSectionTemplateTitle,
|
||||
section_type: newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC',
|
||||
description: newSectionTemplateDescription || undefined,
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM',
|
||||
description: newSectionTemplateDescription || null,
|
||||
default_fields: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
category: newSectionTemplateCategory,
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
};
|
||||
|
||||
console.log('Adding section template:', newTemplate);
|
||||
addSectionTemplate(newTemplate);
|
||||
console.log('Adding section template:', newTemplateData);
|
||||
addSectionTemplate(newTemplateData);
|
||||
|
||||
setNewSectionTemplateTitle('');
|
||||
setNewSectionTemplateDescription('');
|
||||
@@ -1240,9 +1304,10 @@ export function ItemMasterDataManagement() {
|
||||
|
||||
const handleEditSectionTemplate = (template: SectionTemplate) => {
|
||||
setEditingSectionTemplateId(template.id);
|
||||
// SectionTemplate 타입에 맞게 template_name, section_type 사용
|
||||
setNewSectionTemplateTitle(template.template_name);
|
||||
setNewSectionTemplateDescription(template.description || '');
|
||||
setNewSectionTemplateCategory([]);
|
||||
setNewSectionTemplateCategory(template.category || []);
|
||||
setNewSectionTemplateType(template.section_type === 'BOM' ? 'bom' : 'fields');
|
||||
setIsSectionTemplateDialogOpen(true);
|
||||
};
|
||||
@@ -1251,13 +1316,16 @@ export function ItemMasterDataManagement() {
|
||||
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim())
|
||||
return toast.error('섹션 제목을 입력해주세요');
|
||||
|
||||
// Context의 updateSectionTemplate이 기대하는 SectionTemplate 형식 사용
|
||||
// template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
||||
const updateData = {
|
||||
title: newSectionTemplateTitle,
|
||||
template_name: newSectionTemplateTitle,
|
||||
description: newSectionTemplateDescription || undefined,
|
||||
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
|
||||
type: newSectionTemplateType
|
||||
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
};
|
||||
|
||||
console.log('Updating section template:', { id: editingSectionTemplateId, updateData });
|
||||
updateSectionTemplate(editingSectionTemplateId, updateData);
|
||||
|
||||
setEditingSectionTemplateId(null);
|
||||
@@ -1290,19 +1358,20 @@ export function ItemMasterDataManagement() {
|
||||
}
|
||||
|
||||
// 템플릿을 복사해서 섹션으로 추가
|
||||
// API 스펙: SectionTemplate은 title, type ('fields' | 'bom') 사용
|
||||
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
|
||||
page_id: selectedPage.id,
|
||||
section_name: template.template_name,
|
||||
section_type: template.section_type,
|
||||
section_name: template.title,
|
||||
section_type: template.type === 'bom' ? 'BOM' : 'BASIC',
|
||||
description: template.description || undefined,
|
||||
order_no: selectedPage.sections.length + 1,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: [],
|
||||
bomItems: template.section_type === 'BOM' ? [] : undefined
|
||||
bomItems: template.type === 'bom' ? [] : undefined
|
||||
};
|
||||
|
||||
console.log('Loading template to section:', template.template_name, 'type:', template.section_type, 'newSection:', newSection);
|
||||
console.log('Loading template to section:', template.title, 'type:', template.type, 'newSection:', newSection);
|
||||
addSectionToPage(selectedPage.id, newSection);
|
||||
setSelectedTemplateId(null);
|
||||
setIsLoadTemplateDialogOpen(false);
|
||||
@@ -1321,15 +1390,11 @@ export function ItemMasterDataManagement() {
|
||||
// 항목 탭에 해당 항목이 없으면 자동으로 추가
|
||||
const existingMasterField = itemMasterFields.find(f => f.id.toString() === templateFieldKey);
|
||||
if (!existingMasterField && !editingTemplateFieldId) {
|
||||
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
||||
const newMasterField: ItemMasterField = {
|
||||
id: Date.now(),
|
||||
field_name: templateFieldName,
|
||||
field_type: templateFieldInputType === 'textbox' ? 'TEXT' :
|
||||
templateFieldInputType === 'number' ? 'NUMBER' :
|
||||
templateFieldInputType === 'date' ? 'DATE' :
|
||||
templateFieldInputType === 'dropdown' ? 'SELECT' :
|
||||
templateFieldInputType === 'textarea' ? 'TEXTAREA' :
|
||||
templateFieldInputType === 'checkbox' ? 'CHECKBOX' : 'TEXT',
|
||||
field_type: templateFieldInputType,
|
||||
default_properties: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
@@ -1380,38 +1445,30 @@ export function ItemMasterDataManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
const newField: ItemField = {
|
||||
id: editingTemplateFieldId || Date.now(),
|
||||
section_id: 0, // Placeholder for template
|
||||
field_name: templateFieldName,
|
||||
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: {
|
||||
// TemplateField 형식으로 생성 (UI가 기대하는 형식)
|
||||
const newField: TemplateField = {
|
||||
id: String(editingTemplateFieldId || Date.now()),
|
||||
name: templateFieldName,
|
||||
fieldKey: templateFieldKey,
|
||||
property: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
row: 1,
|
||||
col: 1,
|
||||
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
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
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) => f.id === editingTemplateFieldId ? newField : f) : [];
|
||||
// f.id는 string, editingTemplateFieldId는 number이므로 String으로 변환하여 비교
|
||||
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];
|
||||
@@ -1434,18 +1491,19 @@ export function ItemMasterDataManagement() {
|
||||
setIsTemplateFieldDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleEditTemplateField = (templateId: number, field: ItemField) => {
|
||||
// TemplateField 형식으로 수정 (UI가 전달하는 형식)
|
||||
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
|
||||
setCurrentTemplateId(templateId);
|
||||
setEditingTemplateFieldId(field.id);
|
||||
setTemplateFieldName(field.field_name);
|
||||
setTemplateFieldKey(field.id.toString());
|
||||
setTemplateFieldInputType(field.properties?.inputType);
|
||||
setTemplateFieldRequired(field.is_required);
|
||||
setTemplateFieldOptions(field.options?.map(o => o.value).join(', ') || '');
|
||||
setTemplateFieldDescription(field.placeholder || '');
|
||||
setTemplateFieldMultiColumn(field.properties?.multiColumn || false);
|
||||
setTemplateFieldColumnCount(field.properties?.columnCount || 2);
|
||||
setTemplateFieldColumnNames(field.properties?.columnNames || ['컬럼1', '컬럼2']);
|
||||
setEditingTemplateFieldId(Number(field.id)); // TemplateField.id는 string
|
||||
setTemplateFieldName(field.name);
|
||||
setTemplateFieldKey(field.fieldKey);
|
||||
setTemplateFieldInputType(field.property.inputType);
|
||||
setTemplateFieldRequired(field.property.required);
|
||||
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
||||
setTemplateFieldDescription(field.description || '');
|
||||
setTemplateFieldMultiColumn(field.property.multiColumn || false);
|
||||
setTemplateFieldColumnCount(field.property.columnCount || 2);
|
||||
setTemplateFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']);
|
||||
setIsTemplateFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -1456,7 +1514,8 @@ export function ItemMasterDataManagement() {
|
||||
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) => f.id !== fieldId) : [];
|
||||
// f.id는 number 또는 string일 수 있으므로 String으로 변환하여 비교
|
||||
const updatedFields = Array.isArray(currentFields) ? currentFields.filter((f: any) => String(f.id) !== String(fieldId)) : [];
|
||||
updateSectionTemplate(templateId, { default_fields: updatedFields });
|
||||
toast.success('항목이 삭제되었습니다');
|
||||
};
|
||||
@@ -1468,7 +1527,7 @@ export function ItemMasterDataManagement() {
|
||||
id: Date.now(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tenant_id: 1,
|
||||
tenant_id: tenantId ?? 0,
|
||||
section_id: 0
|
||||
};
|
||||
setBomItems(prev => [...prev, newItem]);
|
||||
@@ -1489,7 +1548,7 @@ export function ItemMasterDataManagement() {
|
||||
id: Date.now(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tenant_id: 1,
|
||||
tenant_id: tenantId ?? 0,
|
||||
section_id: 0
|
||||
};
|
||||
|
||||
@@ -1770,11 +1829,7 @@ export function ItemMasterDataManagement() {
|
||||
{ id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 }
|
||||
]);
|
||||
|
||||
setAttributeSubTabs([
|
||||
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
|
||||
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
|
||||
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
|
||||
]);
|
||||
setAttributeSubTabs([]);
|
||||
|
||||
console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다');
|
||||
toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.');
|
||||
@@ -1831,14 +1886,14 @@ export function ItemMasterDataManagement() {
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsManageTabsDialogOpen(true)}
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
탭 관리
|
||||
</Button>
|
||||
{/*<Button*/}
|
||||
{/* size="sm"*/}
|
||||
{/* variant="outline"*/}
|
||||
{/* onClick={() => setIsManageTabsDialogOpen(true)}*/}
|
||||
{/*>*/}
|
||||
{/* <Settings className="h-4 w-4 mr-1" />*/}
|
||||
{/* 탭 관리*/}
|
||||
{/*</Button>*/}
|
||||
{/* 전체 초기화 버튼 숨김 처리 - 디자인에 없는 기능 */}
|
||||
{/* <Button
|
||||
size="sm"
|
||||
@@ -1880,7 +1935,7 @@ export function ItemMasterDataManagement() {
|
||||
className="shrink-0"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-1" />
|
||||
탭 관리
|
||||
항목 관리
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2622,6 +2677,12 @@ export function ItemMasterDataManagement() {
|
||||
newSectionDescription={newSectionDescription}
|
||||
setNewSectionDescription={setNewSectionDescription}
|
||||
handleAddSection={handleAddSection}
|
||||
sectionInputMode={sectionInputMode}
|
||||
setSectionInputMode={setSectionInputMode}
|
||||
sectionTemplates={sectionTemplates}
|
||||
selectedTemplateId={selectedSectionTemplateId}
|
||||
setSelectedTemplateId={setSelectedSectionTemplateId}
|
||||
handleLinkTemplate={handleLinkTemplate}
|
||||
/>
|
||||
|
||||
{/* 항목 추가/수정 다이얼로그 - 데스크톱 */}
|
||||
@@ -2809,6 +2870,14 @@ export function ItemMasterDataManagement() {
|
||||
templateFieldColumnNames={templateFieldColumnNames}
|
||||
setTemplateFieldColumnNames={setTemplateFieldColumnNames}
|
||||
handleAddTemplateField={handleAddTemplateField}
|
||||
// 마스터 항목 관련 props
|
||||
itemMasterFields={itemMasterFields}
|
||||
templateFieldInputMode={templateFieldInputMode}
|
||||
setTemplateFieldInputMode={setTemplateFieldInputMode}
|
||||
showMasterFieldList={templateFieldShowMasterFieldList}
|
||||
setShowMasterFieldList={setTemplateFieldShowMasterFieldList}
|
||||
selectedMasterFieldId={templateFieldSelectedMasterFieldId}
|
||||
setSelectedMasterFieldId={setTemplateFieldSelectedMasterFieldId}
|
||||
/>
|
||||
|
||||
<LoadTemplateDialog
|
||||
|
||||
Reference in New Issue
Block a user