'use client'; 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, TemplateField } from '@/contexts/ItemMasterContext'; import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs'; import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog'; import { type ConditionalFieldConfig } from './ItemMasterDataManagement/components/ConditionalDisplayUI'; import { FieldDrawer } from './ItemMasterDataManagement/dialogs/FieldDrawer'; import { TabManagementDialogs } from './ItemMasterDataManagement/dialogs/TabManagementDialogs'; import { OptionDialog } from './ItemMasterDataManagement/dialogs/OptionDialog'; import { ColumnManageDialog } from './ItemMasterDataManagement/dialogs/ColumnManageDialog'; import { PathEditDialog } from './ItemMasterDataManagement/dialogs/PathEditDialog'; import { PageDialog } from './ItemMasterDataManagement/dialogs/PageDialog'; import { SectionDialog } from './ItemMasterDataManagement/dialogs/SectionDialog'; import { MasterFieldDialog } from './ItemMasterDataManagement/dialogs/MasterFieldDialog'; import { TemplateFieldDialog } from './ItemMasterDataManagement/dialogs/TemplateFieldDialog'; import { LoadTemplateDialog } from './ItemMasterDataManagement/dialogs/LoadTemplateDialog'; import { ColumnDialog } from './ItemMasterDataManagement/dialogs/ColumnDialog'; import { SectionTemplateDialog } from './ItemMasterDataManagement/dialogs/SectionTemplateDialog'; import { itemMasterApi } from '@/lib/api/item-master'; import { getErrorMessage, ApiError } from '@/lib/api/error-handler'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { ErrorMessage } from '@/components/ui/error-message'; import { transformPageResponse, transformPagesResponse, transformSectionTemplatesResponse, transformMasterFieldsResponse, transformCustomTabsResponse, transformUnitOptionsResponse, } from '@/lib/api/transformers'; import { Database, Plus, Trash2, FolderTree, Folder, FileText, Settings, ListTree, Package, Layers } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { toast } from 'sonner'; // 로컬 타입 import import type { OptionColumn, MasterOption } from './ItemMasterDataManagement/types'; // Utils import import { generateAbsolutePath } from './ItemMasterDataManagement/utils/pathUtils'; // 초기 데이터 const INITIAL_UNIT_OPTIONS: MasterOption[] = []; const INITIAL_MATERIAL_OPTIONS: MasterOption[] = []; const INITIAL_SURFACE_TREATMENT_OPTIONS: MasterOption[] = []; const ITEM_TYPE_OPTIONS = [ { value: 'FG', label: '제품 (FG)' }, { value: 'PT', label: '부품 (PT)' }, { value: 'SM', label: '부자재 (SM)' }, { value: 'RM', label: '원자재 (RM)' }, { value: 'CS', label: '소모품 (CS)' } ]; const INPUT_TYPE_OPTIONS = [ { value: 'textbox', label: '텍스트박스' }, { value: 'dropdown', label: '드롭다운' }, { value: 'checkbox', label: '체크박스' }, { value: 'number', label: '숫자' }, { value: 'date', label: '날짜' }, { value: 'textarea', label: '텍스트영역' } ]; export function ItemMasterDataManagement() { const { itemPages, loadItemPages, addItemPage, updateItemPage, deleteItemPage, addSectionToPage, updateSection, deleteSection, addFieldToSection, updateField, deleteField, reorderFields, itemMasterFields, loadItemMasterFields, addItemMasterField, updateItemMasterField, deleteItemMasterField, sectionTemplates, loadSectionTemplates, addSectionTemplate, updateSectionTemplate, deleteSectionTemplate, resetAllData, tenantId } = useItemMaster(); console.log('ItemMasterDataManagement: Current sectionTemplates', sectionTemplates); // 모든 페이지의 섹션을 하나의 배열로 평탄화 const _itemSections = itemPages.flatMap(page => page.sections.map(section => ({ ...section, parentPageId: page.id })) ); // 마운트 상태 추적 (SSR 호환) const [_mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // API 로딩 및 에러 상태 관리 const [isInitialLoading, setIsInitialLoading] = useState(true); // 초기 데이터 로딩 const [_isLoading, setIsLoading] = useState(false); // 개별 작업 로딩 const [error, setError] = useState(null); // 에러 메시지 // 초기 데이터 로딩 useEffect(() => { const loadInitialData = async () => { try { setIsInitialLoading(true); setError(null); const data = await itemMasterApi.init(); // 페이지 데이터 로드 (이미 존재하는 데이터를 state에 로드 - API 호출 없음) const transformedPages = transformPagesResponse(data.pages); loadItemPages(transformedPages); // 섹션 템플릿 로드 (덮어쓰기 - API 호출 없음!) const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates); loadSectionTemplates(transformedTemplates); // 마스터 필드 로드 (덮어쓰기 - API 호출 없음!) const transformedFields = transformMasterFieldsResponse(data.masterFields); loadItemMasterFields(transformedFields); // 커스텀 탭 로드 (local state) if (data.customTabs && data.customTabs.length > 0) { const transformedTabs = transformCustomTabsResponse(data.customTabs); setCustomTabs(prev => [...prev, ...transformedTabs]); } // 단위 옵션 로드 (local state) if (data.unitOptions && data.unitOptions.length > 0) { const transformedUnits = transformUnitOptionsResponse(data.unitOptions); setUnitOptions(transformedUnits); } console.log('✅ Initial data loaded:', { pages: data.pages.length, templates: data.sectionTemplates.length, masterFields: data.masterFields.length, customTabs: data.customTabs?.length || 0, unitOptions: data.unitOptions?.length || 0, }); } catch (err) { if (err instanceof ApiError && err.errors) { // Validation 에러 (422) const errorMessages = Object.entries(err.errors) .map(([field, messages]) => `${field}: ${messages.join(', ')}`) .join('\n'); toast.error(errorMessages); setError('입력값을 확인해주세요.'); } else { const errorMessage = getErrorMessage(err); setError(errorMessage); toast.error(errorMessage); } console.error('❌ Failed to load initial data:', err); } finally { setIsInitialLoading(false); } }; loadInitialData(); }, []); // 동적 탭 관리 - SSR 호환: 항상 기본값으로 시작 const [customTabs, setCustomTabs] = useState>(() => { return [ { id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 }, { id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 }, { id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 }, { id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 } ]; }); const [activeTab, setActiveTab] = useState('hierarchy'); // 속성 하위 탭 관리 (API에서 로드) const [attributeSubTabs, setAttributeSubTabs] = useState>([]); // 마스터 항목이 추가/수정될 때 속성 탭 자동 생성 useEffect(() => { // 새로 추가할 탭들을 먼저 수집 const newTabs: Array<{id: string; label: string; key: string; isDefault: boolean; order: number}> = []; const updatedTabs: Array<{id: string; label: string; key: string; isDefault: boolean; order: number}> = []; itemMasterFields.forEach(field => { // 이미 탭이 있는지 확인 const existingTab = attributeSubTabs.find(tab => tab.key === field.id.toString()); if (!existingTab) { // 새로운 탭 추가 대상 const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), ...newTabs.map(t => t.order), -1); const newTab = { id: `attr-${field.id.toString()}`, label: field.field_name, key: field.id.toString(), isDefault: false, order: maxOrder + 1 }; newTabs.push(newTab); } else if (existingTab.label !== field.field_name) { // 이름이 변경된 경우 updatedTabs.push({ ...existingTab, label: field.field_name }); } }); // 상태 업데이트는 한 번만 수행 if (newTabs.length > 0 || updatedTabs.length > 0) { setAttributeSubTabs(prev => { // 기존 탭 업데이트 let result = prev.map(tab => { const updated = updatedTabs.find(ut => ut.key === tab.key); return updated || tab; }); // 새 탭 추가 result = [...result, ...newTabs]; // 중복 제거 (key 기준) const uniqueTabs = result.filter((tab, index, self) => index === self.findIndex(t => t.key === tab.key) ); return uniqueTabs; }); } }, [itemMasterFields]); const [activeAttributeTab, setActiveAttributeTab] = useState('units'); const [isAddTabDialogOpen, setIsAddTabDialogOpen] = useState(false); const [isManageTabsDialogOpen, setIsManageTabsDialogOpen] = useState(false); const [newTabLabel, setNewTabLabel] = useState(''); const [editingTabId, setEditingTabId] = useState(null); const [deletingTabId, setDeletingTabId] = useState(null); const [isDeleteTabDialogOpen, setIsDeleteTabDialogOpen] = useState(false); // 속성 하위 탭 관리 상태 const [isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen] = useState(false); const [isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen] = useState(false); const [newAttributeTabLabel, setNewAttributeTabLabel] = useState(''); const [editingAttributeTabId, setEditingAttributeTabId] = useState(null); const [deletingAttributeTabId, setDeletingAttributeTabId] = useState(null); const [isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen] = useState(false); const [unitOptions, setUnitOptions] = useState(INITIAL_UNIT_OPTIONS); const [materialOptions, setMaterialOptions] = useState(INITIAL_MATERIAL_OPTIONS); const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState(INITIAL_SURFACE_TREATMENT_OPTIONS); const [customAttributeOptions, setCustomAttributeOptions] = useState>({}); const [isOptionDialogOpen, setIsOptionDialogOpen] = useState(false); const [editingOptionType, setEditingOptionType] = useState(null); const [newOptionValue, setNewOptionValue] = useState(''); const [newOptionLabel, setNewOptionLabel] = useState(''); const [newOptionColumnValues, setNewOptionColumnValues] = useState>({}); // 확장된 입력방식 관련 상태 const [newOptionInputType, setNewOptionInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox'); const [newOptionRequired, setNewOptionRequired] = useState(false); const [newOptionOptions, setNewOptionOptions] = useState(''); // dropdown 옵션 (쉼표 구분) const [newOptionPlaceholder, setNewOptionPlaceholder] = useState(''); const [newOptionDefaultValue, setNewOptionDefaultValue] = useState(''); // 칼럼 관리 상태 const [isColumnManageDialogOpen, setIsColumnManageDialogOpen] = useState(false); const [managingColumnType, setManagingColumnType] = useState(null); const [attributeColumns, setAttributeColumns] = useState>({}); // 칼럼 추가 폼 상태 const [newColumnName, setNewColumnName] = useState(''); const [newColumnKey, setNewColumnKey] = useState(''); const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text'); const [newColumnRequired, setNewColumnRequired] = useState(false); // 계층구조 상태 const [selectedPageId, setSelectedPageId] = useState(itemPages[0]?.id || null); const selectedPage = itemPages.find(p => p.id === selectedPageId); const [_expandedSections, setExpandedSections] = useState>({}); const [editingSectionId, setEditingSectionId] = useState(null); const [editingSectionTitle, setEditingSectionTitle] = useState(''); // 기존 페이지들에 절대경로 자동 생성 (마이그레이션) useEffect(() => { let needsUpdate = false; itemPages.forEach(page => { if (!page.absolute_path) { const absolutePath = generateAbsolutePath(page.item_type, page.page_name); updateItemPage(page.id, { absolute_path: absolutePath }); needsUpdate = true; } }); if (needsUpdate) { console.log('절대경로가 자동으로 생성되었습니다'); } }, []); // 빈 의존성 배열로 최초 1회만 실행 const [editingPageId, setEditingPageId] = useState(null); const [editingPageName, setEditingPageName] = useState(''); const [isPageDialogOpen, setIsPageDialogOpen] = useState(false); const [newPageName, setNewPageName] = useState(''); const [newPageItemType, setNewPageItemType] = useState<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>('FG'); const [editingPathPageId, setEditingPathPageId] = useState(null); const [editingAbsolutePath, setEditingAbsolutePath] = useState(''); const [isSectionDialogOpen, setIsSectionDialogOpen] = useState(false); 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(null); // 모바일 체크 const [isMobile, setIsMobile] = useState(false); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 768); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false); const [selectedSectionForField, setSelectedSectionForField] = useState(null); const [editingFieldId, setEditingFieldId] = useState(null); const [fieldInputMode, setFieldInputMode] = useState<'master' | 'custom'>('custom'); // 마스터 항목 선택 vs 직접 입력 const [showMasterFieldList, setShowMasterFieldList] = useState(false); // 마스터 항목 목록 표시 여부 const [selectedMasterFieldId, setSelectedMasterFieldId] = useState(''); const [newFieldName, setNewFieldName] = useState(''); const [newFieldKey, setNewFieldKey] = useState(''); const [newFieldInputType, setNewFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox'); const [newFieldRequired, setNewFieldRequired] = useState(false); const [newFieldOptions, setNewFieldOptions] = useState(''); const [newFieldDescription, setNewFieldDescription] = useState(''); // 텍스트박스 컬럼 관리 const [textboxColumns, setTextboxColumns] = useState>([]); const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false); const [editingColumnId, setEditingColumnId] = useState(null); const [columnName, setColumnName] = useState(''); const [columnKey, setColumnKey] = useState(''); // 조건부 항목 상태 const [newFieldConditionEnabled, setNewFieldConditionEnabled] = useState(false); const [newFieldConditionTargetType, setNewFieldConditionTargetType] = useState<'field' | 'section'>('field'); const [newFieldConditionFields, setNewFieldConditionFields] = useState([]); const [newFieldConditionSections, setNewFieldConditionSections] = useState([]); // 임시 입력용 const [_tempConditionFieldKey, setTempConditionFieldKey] = useState(''); const [tempConditionValue, setTempConditionValue] = useState(''); // 마스터 항목 관리 상태 const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false); const [editingMasterFieldId, setEditingMasterFieldId] = useState(null); const [newMasterFieldName, setNewMasterFieldName] = useState(''); const [newMasterFieldKey, setNewMasterFieldKey] = useState(''); const [newMasterFieldInputType, setNewMasterFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox'); const [newMasterFieldRequired, setNewMasterFieldRequired] = useState(false); const [newMasterFieldCategory, setNewMasterFieldCategory] = useState('공통'); const [newMasterFieldDescription, setNewMasterFieldDescription] = useState(''); const [newMasterFieldOptions, setNewMasterFieldOptions] = useState(''); const [newMasterFieldAttributeType, setNewMasterFieldAttributeType] = useState<'custom' | 'unit' | 'material' | 'surface'>('custom'); const [newMasterFieldMultiColumn, setNewMasterFieldMultiColumn] = useState(false); const [newMasterFieldColumnCount, setNewMasterFieldColumnCount] = useState(2); const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState(['컬럼1', '컬럼2']); // 섹션 템플릿 관리 상태 const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false); const [editingSectionTemplateId, setEditingSectionTemplateId] = useState(null); const [newSectionTemplateTitle, setNewSectionTemplateTitle] = useState(''); const [newSectionTemplateDescription, setNewSectionTemplateDescription] = useState(''); const [newSectionTemplateCategory, setNewSectionTemplateCategory] = useState([]); const [newSectionTemplateType, setNewSectionTemplateType] = useState<'fields' | 'bom'>('fields'); // 섹션 템플릿 불러오기 다이얼로그 const [isLoadTemplateDialogOpen, setIsLoadTemplateDialogOpen] = useState(false); const [selectedTemplateId, setSelectedTemplateId] = useState(null); // 섹션 템플릿 확장 상태 const [_expandedTemplateId, _setExpandedTemplateId] = useState(null); // 섹션 템플릿 항목 추가 다이얼로그 const [isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen] = useState(false); const [currentTemplateId, setCurrentTemplateId] = useState(null); const [editingTemplateFieldId, setEditingTemplateFieldId] = useState(null); const [templateFieldName, setTemplateFieldName] = useState(''); const [templateFieldKey, setTemplateFieldKey] = useState(''); const [templateFieldInputType, setTemplateFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox'); const [templateFieldRequired, setTemplateFieldRequired] = useState(false); const [templateFieldOptions, setTemplateFieldOptions] = useState(''); const [templateFieldDescription, setTemplateFieldDescription] = useState(''); const [templateFieldMultiColumn, setTemplateFieldMultiColumn] = useState(false); const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2); const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState(['컬럼1', '컬럼2']); // 템플릿 필드 마스터 항목 관련 상태 const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom'); const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false); const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState(''); // BOM 관리 상태 const [_bomItems, setBomItems] = useState([]); // 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트 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[] = []; if (attributeType === 'unit') { newOptions = unitOptions.map(opt => opt.label); } else if (attributeType === 'material') { newOptions = materialOptions.map(opt => opt.label); } else if (attributeType === 'surface') { newOptions = surfaceTreatmentOptions.map(opt => opt.label); } else { // 사용자 정의 속성 const customOptions = customAttributeOptions[attributeType] || []; newOptions = customOptions.map(opt => opt.label); } const currentOptions = field.default_properties?.options || []; const optionsChanged = JSON.stringify(currentOptions.sort()) !== JSON.stringify(newOptions.sort()); if (optionsChanged && newOptions.length > 0) { updateItemMasterField(field.id, { ...field, default_properties: { ...(field.default_properties || {}), options: newOptions } }); } } }); }, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions, itemMasterFields]); const handleAddOption = () => { if (!editingOptionType || !newOptionValue.trim() || !newOptionLabel.trim()) return toast.error('모든 항목을 입력해주세요'); // dropdown일 경우 옵션 필수 체크 if (newOptionInputType === 'dropdown' && !newOptionOptions.trim()) { return toast.error('드롭다운 옵션을 입력해주세요'); } // 칼럼 필수 값 체크 const currentColumns = attributeColumns[editingOptionType] || []; for (const column of currentColumns) { if (column.required && !newOptionColumnValues[column.key]?.trim()) { return toast.error(`${column.name}은(는) 필수 입력 항목입니다`); } } const newOption: MasterOption = { id: `${editingOptionType}-${Date.now()}`, value: newOptionValue, label: newOptionLabel, isActive: true, inputType: newOptionInputType, required: newOptionRequired, options: newOptionInputType === 'dropdown' ? newOptionOptions.split(',').map(o => o.trim()).filter(o => o) : undefined, placeholder: newOptionPlaceholder || undefined, defaultValue: newOptionDefaultValue || undefined, columnValues: Object.keys(newOptionColumnValues).length > 0 ? { ...newOptionColumnValues } : undefined }; if (editingOptionType === 'unit') { setUnitOptions([...unitOptions, newOption]); } else if (editingOptionType === 'material') { setMaterialOptions([...materialOptions, newOption]); } else if (editingOptionType === 'surface') { setSurfaceTreatmentOptions([...surfaceTreatmentOptions, newOption]); } else { // 사용자 정의 속성 탭 setCustomAttributeOptions(prev => ({ ...prev, [editingOptionType]: [...(prev[editingOptionType] || []), newOption] })); } setNewOptionValue(''); setNewOptionLabel(''); setNewOptionColumnValues({}); setNewOptionInputType('textbox'); setNewOptionRequired(false); setNewOptionOptions(''); setNewOptionPlaceholder(''); setNewOptionDefaultValue(''); setIsOptionDialogOpen(false); toast.success('속성이 추가되었습니다 (저장 필요)'); }; const handleDeleteOption = (type: string, id: string) => { if (type === 'unit') { setUnitOptions(unitOptions.filter(o => o.id !== id)); } else if (type === 'material') { setMaterialOptions(materialOptions.filter(o => o.id !== id)); } else if (type === 'surface') { setSurfaceTreatmentOptions(surfaceTreatmentOptions.filter(o => o.id !== id)); } else { // 사용자 정의 속성 탭 setCustomAttributeOptions(prev => ({ ...prev, [type]: (prev[type] || []).filter(o => o.id !== id) })); } toast.success('삭제되었습니다'); }; // 절대경로 자동 생성 함수 // 계층구조 핸들러 const handleAddPage = async () => { if (!newPageName.trim()) return toast.error('섹션명을 입력해주세요'); try { setIsLoading(true); const absolutePath = generateAbsolutePath(newPageItemType, newPageName); // 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, }); // 새로 생성된 페이지를 선택 setSelectedPageId(newPage.id); // 폼 초기화 setNewPageName(''); setNewPageItemType('FG'); setIsPageDialogOpen(false); toast.success('페이지가 추가되었습니다'); } catch (err) { if (err instanceof ApiError && err.errors) { // Validation 에러 (422) const errorMessages = Object.entries(err.errors) .map(([field, messages]) => `${field}: ${messages.join(', ')}`) .join('\n'); toast.error(errorMessages); } else { const errorMessage = getErrorMessage(err); toast.error(errorMessage); } console.error('❌ Failed to create page:', err); } finally { setIsLoading(false); } }; const handleDuplicatePage = async (pageId: number) => { const originalPage = itemPages.find(p => p.id === pageId); if (!originalPage) return toast.error('페이지를 찾을 수 없습니다'); try { setIsLoading(true); // 페이지 복제 const duplicatedPageName = `${originalPage.page_name} (복제)`; const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName); // 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 = () => { if (!selectedPage || !newSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요'); const sectionType: 'BASIC' | 'BOM' | 'CUSTOM' = newSectionType === 'bom' ? 'BOM' : 'BASIC'; const newSection: ItemSection = { id: Date.now(), page_id: selectedPage.id, section_name: newSectionTitle, section_type: sectionType, description: newSectionDescription || undefined, order_no: selectedPage.sections.length + 1, is_collapsible: true, is_default_open: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), fields: [], bomItems: sectionType === 'BOM' ? [] : undefined }; console.log('Adding section to page:', { pageId: selectedPage.id, page_name: selectedPage.page_name, sectionTitle: newSection.section_name, sectionType: newSection.section_type, currentSectionCount: selectedPage.sections.length, newSection: newSection }); // 1. 페이지에 섹션 추가 addSectionToPage(selectedPage.id, newSection); // 섹션은 페이지의 일부이므로 sections로 별도 추적하지 않음 // 2. 섹션관리 탭에도 템플릿으로 자동 추가 (계층구조 섹션 = 섹션 탭 섹션) // 프론트엔드 형식: template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM') const newTemplateData = { tenant_id: tenantId ?? 0, template_name: newSection.section_name, section_type: newSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM', description: newSection.description ?? null, default_fields: null, created_by: null, updated_by: null, }; addSectionTemplate(newTemplateData); console.log('Section added to both page and template:', { sectionId: newSection.id, templateTitle: newTemplateData.title }); setNewSectionTitle(''); setNewSectionDescription(''); setNewSectionType('fields'); setIsSectionDialogOpen(false); toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`); }; // 섹션 템플릿을 페이지에 연결 (SectionDialog에서 사용) const handleLinkTemplate = (template: SectionTemplate) => { if (!selectedPage) { toast.error('페이지를 먼저 선택해주세요'); return; } // 템플릿을 섹션으로 변환하여 페이지에 추가 const newSection: Omit = { 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); }; const handleSaveSectionTitle = () => { if (!selectedPage || !editingSectionId || !editingSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요'); updateSection(Number(editingSectionId), { section_name: editingSectionTitle }); setEditingSectionId(null); toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)'); }; const _handleMoveSectionUp = (sectionId: number) => { if (!selectedPage) return; const sections = [...selectedPage.sections]; const index = sections.findIndex(s => s.id === sectionId); if (index <= 0) return; // 첫 번째 섹션이거나 못 찾음 // 배열에서 위치 교환 [sections[index - 1], sections[index]] = [sections[index], sections[index - 1]]; // order 값 재설정 const updatedSections = sections.map((section, idx) => ({ ...section, order: idx + 1 })); // 페이지 업데이트 updateItemPage(selectedPage.id, { sections: updatedSections }); toast.success('섹션 순서가 변경되었습니다'); }; const _handleMoveSectionDown = (sectionId: number) => { if (!selectedPage) return; const sections = [...selectedPage.sections]; const index = sections.findIndex(s => s.id === sectionId); if (index < 0 || index >= sections.length - 1) return; // 마지막 섹션이거나 못 찾음 // 배열에서 위치 교환 [sections[index], sections[index + 1]] = [sections[index + 1], sections[index]]; // order 값 재설정 const updatedSections = sections.map((section, idx) => ({ ...section, order: idx + 1 })); // 페이지 업데이트 updateItemPage(selectedPage.id, { sections: updatedSections }); toast.success('섹션 순서가 변경되었습니다'); }; const handleAddField = () => { if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim()) return toast.error('모든 필수 항목을 입력해주세요'); // 조건부 표시 설정 const displayCondition: FieldDisplayCondition | undefined = newFieldConditionEnabled ? { targetType: newFieldConditionTargetType, fieldConditions: newFieldConditionTargetType === 'field' && newFieldConditionFields.length > 0 ? newFieldConditionFields : undefined, sectionIds: newFieldConditionTargetType === 'section' && newFieldConditionSections.length > 0 ? newFieldConditionSections : undefined } : undefined; // 텍스트박스 컬럼 설정 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, is_required: newFieldRequired, placeholder: newFieldDescription || null, default_value: null, display_condition: displayCondition as Record | null || null, validation_rules: null, options: newFieldInputType === 'dropdown' && newFieldOptions.trim() ? newFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() })) : null, properties: hasColumns ? { multiColumn: true, columnCount: textboxColumns.length, columnNames: textboxColumns.map(c => c.name) } : null, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; if (editingFieldId) { console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.field_name }); updateField(Number(editingFieldId), newField); // 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면) const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name); if (existingMasterField) { const updatedMasterField: ItemMasterField = { ...existingMasterField, field_name: newField.field_name, description: newField.placeholder, default_properties: newField.properties, updated_at: new Date().toISOString() }; updateItemMasterField(existingMasterField.id, updatedMasterField); } toast.success('항목이 섹션에 수정되었습니다!'); } else { console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.field_name }); // 1. 섹션에 항목 추가 addFieldToSection(Number(selectedSectionForField), newField); // 2. 마스터 항목 선택이 아닌 경우에만 새 마스터 항목 자동 생성 // (마스터 항목 선택 시에는 이미 master_field_id로 연결되어 있음) const isFromMasterField = masterFieldId !== null; const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name); 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, // API 스펙에 맞게 소문자 그대로 전달 description: newField.placeholder, default_properties: newField.properties, category: selectedPage.item_type, // 현재 페이지의 품목유형을 카테고리로 설정 created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; addItemMasterField(newMasterField); console.log('Field added to both section and master fields:', { fieldId: newField.id, masterFieldId: newMasterField.id }); // 3. dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가 if (newField.properties?.inputType === 'dropdown' && newField.options && newField.options.length > 0) { const existingCustomOptions = customAttributeOptions[newField.field_name]; if (!existingCustomOptions || existingCustomOptions.length === 0) { const customOptions = newField.options.map((option: { label: string; value: string }, index: number) => ({ id: `CUSTOM-${newField.field_name}-${Date.now()}-${index}`, value: option.value, label: option.label, isActive: true })); setCustomAttributeOptions(prev => ({ ...prev, [newField.field_name]: customOptions })); // 속성관리 탭에 하위 탭으로 추가 const existingTab = attributeSubTabs.find(tab => tab.key === newField.field_name); if (!existingTab) { const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1); const newTab = { id: `attr-${newField.field_name}`, label: newField.field_name, key: newField.field_name, isDefault: false, order: maxOrder + 1 }; setAttributeSubTabs(prev => { // 추가 전 중복 체크 (혹시 모를 race condition 대비) const alreadyExists = prev.find(t => t.key === newTab.key); if (alreadyExists) return prev; const updated = [...prev, newTab]; // 중복 제거 return updated.filter((tab, index, self) => index === self.findIndex(t => t.key === tab.key) ); }); console.log('New attribute tab added:', newTab); } console.log('Dropdown options added to custom attributes:', { attributeKey: newField.field_name, options: customOptions }); toast.success(`항목이 추가되고 "${newField.field_name}" 속성 탭이 속성관리에 등록되었습니다!`); } else { toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!'); } } else { toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!'); } } else { toast.success('항목이 섹션에 추가되었습니다! (이미 마스터 항목에 존재함)'); } } // 폼 초기화 setNewFieldName(''); setNewFieldKey(''); setNewFieldInputType('textbox'); setNewFieldRequired(false); setNewFieldOptions(''); setNewFieldDescription(''); setNewFieldConditionEnabled(false); setNewFieldConditionTargetType('field'); setNewFieldConditionFields([]); setNewFieldConditionSections([]); setTempConditionFieldKey(''); setTempConditionValue(''); setEditingFieldId(null); setIsFieldDialogOpen(false); }; const handleEditField = (sectionId: string, field: ItemField) => { setSelectedSectionForField(Number(sectionId)); setEditingFieldId(field.id); setNewFieldName(field.field_name); setNewFieldKey(field.id.toString()); setNewFieldInputType(field.field_type); setNewFieldRequired(field.is_required); setNewFieldOptions(field.options?.map(opt => opt.value).join(', ') || ''); setNewFieldDescription(''); // description은 ItemField에 없음 // 조건부 표시 설정 로드 if (field.display_condition) { setNewFieldConditionEnabled(true); setNewFieldConditionTargetType(field.display_condition.targetType); setNewFieldConditionFields(field.display_condition.fieldConditions || []); setNewFieldConditionSections(field.display_condition.sectionIds || []); } else { setNewFieldConditionEnabled(false); setNewFieldConditionTargetType('field'); setNewFieldConditionFields([]); setNewFieldConditionSections([]); } setIsFieldDialogOpen(true); }; // 마스터 필드 선택 시 폼 자동 채우기 useEffect(() => { if (fieldInputMode === 'master' && selectedMasterFieldId) { const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId)); if (masterField) { setNewFieldName(masterField.field_name); setNewFieldKey(masterField.id.toString()); setNewFieldInputType(masterField.default_properties?.inputType); setNewFieldRequired(masterField.default_properties?.required); setNewFieldOptions(masterField.default_properties?.options?.join(', ') || ''); setNewFieldDescription(masterField.description || ''); } } else if (fieldInputMode === 'custom') { // 직접 입력 모드로 전환 시 폼 초기화 setNewFieldName(''); setNewFieldKey(''); setNewFieldInputType('textbox'); setNewFieldRequired(false); setNewFieldOptions(''); setNewFieldDescription(''); } }, [fieldInputMode, selectedMasterFieldId, itemMasterFields]); // 마스터 항목 관리 핸들러 const handleAddMasterField = () => { if (!newMasterFieldName.trim() || !newMasterFieldKey.trim()) return toast.error('항목명과 필드 키를 입력해주세요'); // 속성 목록 초기화 (dropdown 타입이고 옵션이 있으면 각 옵션을 속성으로 추가) let _properties: ItemFieldProperty[] = []; if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) { const options = newMasterFieldOptions.split(',').map(o => o.trim()); _properties = options.map((opt, idx) => ({ id: `prop-${Date.now()}-${idx}`, key: `${newMasterFieldKey}_${opt.toLowerCase().replace(/\s+/g, '_')}`, label: opt, type: 'textbox', inputType: 'textbox', required: false, row: idx + 1, col: 1 })); } // API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea) const newMasterField: ItemMasterField = { id: Date.now(), field_name: newMasterFieldName, field_type: newMasterFieldInputType, category: newMasterFieldCategory || null, description: newMasterFieldDescription || null, default_validation: null, default_properties: { inputType: newMasterFieldInputType, required: newMasterFieldRequired, options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim() ? newMasterFieldOptions.split(',').map(o => o.trim()) : undefined, attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined, multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined, columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined, columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined }, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; addItemMasterField(newMasterField); // dropdown 타입이고 attributeType이 'custom'이며 옵션이 있으면 속성관리 탭에도 자동 추가 if (newMasterFieldInputType === 'dropdown' && newMasterFieldAttributeType === 'custom' && newMasterFieldOptions.trim()) { const options = newMasterFieldOptions.split(',').map(o => o.trim()); const existingCustomOptions = customAttributeOptions[newMasterFieldKey]; if (!existingCustomOptions || existingCustomOptions.length === 0) { const customOptions = options.map((option, index) => ({ id: `CUSTOM-${newMasterFieldKey}-${Date.now()}-${index}`, value: option, label: option, isActive: true })); setCustomAttributeOptions(prev => ({ ...prev, [newMasterFieldKey]: customOptions })); // 속성관리 탭에 하위 탭으로 추가 const existingTab = attributeSubTabs.find(tab => tab.key === newMasterFieldKey); if (!existingTab) { const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1); const newTab = { id: `attr-${newMasterFieldKey}`, label: newMasterFieldName, key: newMasterFieldKey, isDefault: false, order: maxOrder + 1 }; setAttributeSubTabs(prev => { // 추가 전 중복 체크 (혹시 모를 race condition 대비) const alreadyExists = prev.find(t => t.key === newTab.key); if (alreadyExists) return prev; const updated = [...prev, newTab]; // 중복 제거 return updated.filter((tab, index, self) => index === self.findIndex(t => t.key === tab.key) ); }); console.log('New attribute tab added from master field:', newTab); } console.log('Master field dropdown options added to custom attributes:', { attributeKey: newMasterFieldKey, options: customOptions }); } } // 폼 초기화 setNewMasterFieldName(''); setNewMasterFieldKey(''); setNewMasterFieldInputType('textbox'); setNewMasterFieldRequired(false); setNewMasterFieldCategory('공통'); setNewMasterFieldDescription(''); setNewMasterFieldOptions(''); setNewMasterFieldAttributeType('custom'); setNewMasterFieldMultiColumn(false); setNewMasterFieldColumnCount(2); setNewMasterFieldColumnNames(['컬럼1', '컬럼2']); setIsMasterFieldDialogOpen(false); toast.success('마스터 항목이 추가되었습니다 (속성 탭에 반영됨, 저장 필요)'); }; const handleEditMasterField = (field: ItemMasterField) => { setEditingMasterFieldId(field.id); setNewMasterFieldName(field.field_name); setNewMasterFieldKey(field.id.toString()); setNewMasterFieldInputType(field.default_properties?.inputType); setNewMasterFieldRequired(field.default_properties?.required); setNewMasterFieldCategory(field.category || '공통'); setNewMasterFieldDescription(field.description || ''); setNewMasterFieldOptions(field.default_properties?.options?.join(', ') || ''); setNewMasterFieldAttributeType((field.default_properties as any).attributeType || 'custom'); setNewMasterFieldMultiColumn(field.default_properties?.multiColumn || false); setNewMasterFieldColumnCount(field.default_properties?.columnCount || 2); setNewMasterFieldColumnNames(field.default_properties?.columnNames || ['컬럼1', '컬럼2']); setIsMasterFieldDialogOpen(true); }; const handleUpdateMasterField = () => { if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim()) return toast.error('항목명과 필드 키를 입력해주세요'); // 속성 목록 업데이트 (dropdown 타입이고 옵션이 있으면 각 옵션을 속성으로 추가) let _properties2: ItemFieldProperty[] = []; if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) { const options = newMasterFieldOptions.split(',').map(o => o.trim()); _properties2 = options.map((opt, idx) => ({ id: `prop-${Date.now()}-${idx}`, key: `${newMasterFieldKey}_${opt.toLowerCase().replace(/\s+/g, '_')}`, label: opt, type: 'textbox', inputType: 'textbox', required: false, row: idx + 1, col: 1 })); } updateItemMasterField(editingMasterFieldId, { field_name: newMasterFieldName, default_properties: { inputType: newMasterFieldInputType, required: newMasterFieldRequired, row: 1, col: 1, options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim() ? newMasterFieldOptions.split(',').map(o => o.trim()) : undefined, attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined, multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined, columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined, columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined }, category: newMasterFieldCategory, description: newMasterFieldDescription || undefined }); // 폼 초기화 setEditingMasterFieldId(null); setNewMasterFieldName(''); setNewMasterFieldKey(''); setNewMasterFieldInputType('textbox'); setNewMasterFieldRequired(false); setNewMasterFieldCategory('공통'); setNewMasterFieldDescription(''); setNewMasterFieldOptions(''); setNewMasterFieldAttributeType('custom'); setNewMasterFieldMultiColumn(false); setNewMasterFieldColumnCount(2); setNewMasterFieldColumnNames(['컬럼1', '컬럼2']); setIsMasterFieldDialogOpen(false); toast.success('마스터 항목이 수정되었습니다 (속성 탭에 반영됨, 저장 필요)'); }; const handleDeleteMasterField = (id: number) => { if (confirm('이 마스터 항목을 삭제하시겠습니까?')) { // 삭제할 마스터 항목 찾기 const fieldToDelete = itemMasterFields.find(f => f.id === id); // 마스터 항목 삭제 deleteItemMasterField(id); // 속성 탭에서 해당 탭 제거 if (fieldToDelete) { setAttributeSubTabs(prev => prev.filter(tab => tab.key !== fieldToDelete.id.toString())); // 삭제된 탭이 현재 활성 탭이면 다른 탭으로 전환 if (activeAttributeTab === fieldToDelete.id.toString()) { setActiveAttributeTab('units'); } } toast.success('마스터 항목이 삭제되었습니다'); } }; // 페이지 삭제 핸들러 (pendingChanges 제거 포함) const handleDeletePageWithTracking = (pageId: number) => { // 삭제할 페이지 찾기 const pageToDelete = itemPages.find(p => p.id === pageId); // 해당 페이지의 모든 섹션 ID 수집 const sectionIds = pageToDelete?.sections.map(s => s.id) || []; // 해당 페이지의 모든 필드 ID 수집 const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || []; // ItemMasterContext의 deleteItemPage 호출 deleteItemPage(pageId); console.log('페이지 삭제 완료:', { pageId, removedSections: sectionIds.length, removedFields: fieldIds.length }); }; // 섹션 삭제 핸들러 (pendingChanges 제거 포함) const handleDeleteSectionWithTracking = (pageId: number, sectionId: number) => { // 삭제할 섹션 찾기 const page = itemPages.find(p => p.id === pageId); const sectionToDelete = page?.sections.find(s => s.id === sectionId); // 해당 섹션의 모든 필드 ID 수집 const fieldIds = sectionToDelete?.fields?.map(f => f.id) || []; // ItemMasterContext의 deleteSection 호출 deleteSection(Number(sectionId)); console.log('섹션 삭제 완료:', { sectionId, removedFields: fieldIds.length }); }; // 필드 삭제 핸들러 (pendingChanges 제거 포함) const handleDeleteFieldWithTracking = (pageId: string, sectionId: string, fieldId: string) => { // ItemMasterContext의 deleteField 호출 deleteField(Number(fieldId)); console.log('필드 삭제 완료:', fieldId); }; // 섹션 템플릿 핸들러 const handleAddSectionTemplate = () => { if (!newSectionTemplateTitle.trim()) return toast.error('섹션 제목을 입력해주세요'); // 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') as 'BASIC' | 'BOM' | 'CUSTOM', description: newSectionTemplateDescription || null, default_fields: null, category: newSectionTemplateCategory, created_by: null, updated_by: null, }; console.log('Adding section template:', newTemplateData); addSectionTemplate(newTemplateData); setNewSectionTemplateTitle(''); setNewSectionTemplateDescription(''); setNewSectionTemplateCategory([]); setNewSectionTemplateType('fields'); setIsSectionTemplateDialogOpen(false); toast.success('섹션 템플릿이 추가되었습니다! (템플릿 목록에서 확인 가능)'); }; const handleEditSectionTemplate = (template: SectionTemplate) => { setEditingSectionTemplateId(template.id); // SectionTemplate 타입에 맞게 template_name, section_type 사용 setNewSectionTemplateTitle(template.template_name); setNewSectionTemplateDescription(template.description || ''); setNewSectionTemplateCategory(template.category || []); setNewSectionTemplateType(template.section_type === 'BOM' ? 'bom' : 'fields'); setIsSectionTemplateDialogOpen(true); }; const handleUpdateSectionTemplate = () => { if (!editingSectionTemplateId || !newSectionTemplateTitle.trim()) return toast.error('섹션 제목을 입력해주세요'); // Context의 updateSectionTemplate이 기대하는 SectionTemplate 형식 사용 // template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM') const updateData = { template_name: 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); setEditingSectionTemplateId(null); setNewSectionTemplateTitle(''); setNewSectionTemplateDescription(''); setNewSectionTemplateCategory([]); setNewSectionTemplateType('fields'); setIsSectionTemplateDialogOpen(false); toast.success('섹션이 수정되었습니다 (저장 필요)'); }; const handleDeleteSectionTemplate = (id: number) => { if (confirm('이 섹션을 삭제하시겠습니까?')) { // 섹션 템플릿 삭제 deleteSectionTemplate(id); toast.success('섹션이 삭제되었습니다'); } }; // 섹션 템플릿 불러오기 const handleLoadTemplate = () => { if (!selectedTemplateId || !selectedPage) { return toast.error('템플릿을 선택해주세요'); } const template = sectionTemplates.find(t => t.id === Number(selectedTemplateId)); if (!template) { return toast.error('템플릿을 찾을 수 없습니다'); } // 템플릿을 복사해서 섹션으로 추가 // API 스펙: SectionTemplate은 title, type ('fields' | 'bom') 사용 const newSection: Omit = { page_id: selectedPage.id, 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.type === 'bom' ? [] : undefined }; console.log('Loading template to section:', template.title, 'type:', template.type, 'newSection:', newSection); addSectionToPage(selectedPage.id, newSection); setSelectedTemplateId(null); setIsLoadTemplateDialogOpen(false); toast.success('섹션이 불러와졌습니다'); }; // 섹션 템플릿 항목 추가 const handleAddTemplateField = () => { if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) { return toast.error('모든 필수 항목을 입력해주세요'); } const template = sectionTemplates.find(t => t.id === currentTemplateId); if (!template) return; // 항목 탭에 해당 항목이 없으면 자동으로 추가 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, default_properties: { 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 } as any, category: '공통', description: templateFieldDescription || undefined, created_at: new Date().toISOString().split('T')[0], updated_at: new Date().toISOString().split('T')[0] }; addItemMasterField(newMasterField); // dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가 if (templateFieldInputType === 'dropdown' && templateFieldOptions.trim()) { const options = templateFieldOptions.split(',').map(o => o.trim()); const existingCustomOptions = customAttributeOptions[templateFieldKey]; if (!existingCustomOptions || existingCustomOptions.length === 0) { const customOptions = options.map((option, index) => ({ id: `CUSTOM-${templateFieldKey}-${Date.now()}-${index}`, value: option, label: option, isActive: true })); setCustomAttributeOptions(prev => ({ ...prev, [templateFieldKey]: customOptions })); console.log('Template field dropdown options added to custom attributes:', { attributeKey: templateFieldKey, options: customOptions }); toast.success(`항목 탭과 속성관리 탭에 "${templateFieldName}" 속성이 자동으로 추가되었습니다`); } else { toast.success('항목 탭에 자동으로 추가되었습니다'); } } else { toast.success('항목 탭에 자동으로 추가되었습니다'); } } // TemplateField 형식으로 생성 (UI가 기대하는 형식) 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) { // 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]; toast.success('항목이 추가되었습니다'); } updateSectionTemplate(currentTemplateId, { default_fields: updatedFields }); // 폼 초기화 setTemplateFieldName(''); setTemplateFieldKey(''); setTemplateFieldInputType('textbox'); setTemplateFieldRequired(false); setTemplateFieldOptions(''); setTemplateFieldDescription(''); setTemplateFieldMultiColumn(false); setTemplateFieldColumnCount(2); setTemplateFieldColumnNames(['컬럼1', '컬럼2']); setEditingTemplateFieldId(null); setIsTemplateFieldDialogOpen(false); }; // TemplateField 형식으로 수정 (UI가 전달하는 형식) const handleEditTemplateField = (templateId: number, field: TemplateField) => { setCurrentTemplateId(templateId); 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); }; const handleDeleteTemplateField = (templateId: number, fieldId: string) => { if (!confirm('이 항목을 삭제하시겠습니까?')) 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) : []; // 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('항목이 삭제되었습니다'); }; // BOM 관리 핸들러 const _handleAddBOMItem = (item: Omit) => { const newItem: BOMItem = { ...item, id: Date.now(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), tenant_id: tenantId ?? 0, section_id: 0 }; setBomItems(prev => [...prev, newItem]); }; const _handleUpdateBOMItem = (id: number, item: Partial) => { setBomItems(prev => prev.map(bom => bom.id === id ? { ...bom, ...item } : bom)); }; const _handleDeleteBOMItem = (id: number) => { setBomItems(prev => prev.filter(bom => bom.id !== id)); }; // 템플릿별 BOM 관리 핸들러 const handleAddBOMItemToTemplate = (templateId: number, item: Omit) => { 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 }); }; const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial) => { 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 }); }; 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 }); }; const _toggleSection = (sectionId: string) => { setExpandedSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] })); }; // 탭 관리 함수 const handleAddTab = () => { if (!newTabLabel.trim()) return toast.error('탭 이름을 입력해주세요'); const newTab = { id: Date.now().toString(), label: newTabLabel, icon: 'FileText', isDefault: false, order: customTabs.length + 1 }; setCustomTabs(prev => [...prev, newTab]); setNewTabLabel(''); setIsAddTabDialogOpen(false); toast.success('탭이 추가되었습니다'); }; const _handleEditTab = (tabId: string) => { const tab = customTabs.find(t => t.id === tabId); if (!tab || tab.isDefault) return; setEditingTabId(tabId); setNewTabLabel(tab.label); setIsAddTabDialogOpen(true); }; const handleUpdateTab = () => { if (!newTabLabel.trim() || !editingTabId) return toast.error('탭 이름을 입력해주세요'); setCustomTabs(prev => prev.map(tab => tab.id === editingTabId ? { ...tab, label: newTabLabel } : tab )); setEditingTabId(null); setNewTabLabel(''); setIsAddTabDialogOpen(false); setIsManageTabsDialogOpen(true); toast.success('탭이 수정되었습니다'); }; const handleDeleteTab = (tabId: string) => { const tab = customTabs.find(t => t.id === tabId); if (!tab || tab.isDefault) return toast.error('기본 탭은 삭제할 수 없습니다'); setDeletingTabId(tabId); setIsDeleteTabDialogOpen(true); }; const confirmDeleteTab = () => { if (!deletingTabId) return; setCustomTabs(prev => prev.filter(t => t.id !== deletingTabId)); if (activeTab === deletingTabId) setActiveTab('categories'); setIsDeleteTabDialogOpen(false); setDeletingTabId(null); toast.success('탭이 삭제되었습니다'); }; // 속성 하위 탭 관리 함수들 const handleAddAttributeTab = () => { if (!newAttributeTabLabel.trim()) return toast.error('탭 이름을 입력해주세요'); const newTab = { id: `attr-${Date.now()}`, label: newAttributeTabLabel, key: `custom-${Date.now()}`, isDefault: false, order: attributeSubTabs.length }; setAttributeSubTabs(prev => [...prev, newTab]); setNewAttributeTabLabel(''); setIsAddAttributeTabDialogOpen(false); toast.success('속성 탭이 추가되었습니다'); }; const handleUpdateAttributeTab = () => { if (!newAttributeTabLabel.trim() || !editingAttributeTabId) return toast.error('탭 이름을 입력해주세요'); setAttributeSubTabs(prev => prev.map(tab => tab.id === editingAttributeTabId ? { ...tab, label: newAttributeTabLabel } : tab )); setEditingAttributeTabId(null); setNewAttributeTabLabel(''); setIsAddAttributeTabDialogOpen(false); setIsManageAttributeTabsDialogOpen(true); toast.success('속성 탭이 수정되었습니다'); }; const handleDeleteAttributeTab = (tabId: string) => { const tab = attributeSubTabs.find(t => t.id === tabId); if (!tab || tab.isDefault) return toast.error('기본 속성 탭은 삭제할 수 없습니다'); setDeletingAttributeTabId(tabId); setIsDeleteAttributeTabDialogOpen(true); }; const confirmDeleteAttributeTab = () => { if (!deletingAttributeTabId) return; setAttributeSubTabs(prev => prev.filter(t => t.id !== deletingAttributeTabId)); if (activeAttributeTab === deletingAttributeTabId) { const firstTab = attributeSubTabs.find(t => t.id !== deletingAttributeTabId); if (firstTab) setActiveAttributeTab(firstTab.key); } setIsDeleteAttributeTabDialogOpen(false); setDeletingAttributeTabId(null); toast.success('속성 탭이 삭제되었습니다'); }; const moveAttributeTabUp = (tabId: string) => { const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId); if (tabIndex <= 0) return; const newTabs = [...attributeSubTabs]; const temp = newTabs[tabIndex - 1].order; newTabs[tabIndex - 1].order = newTabs[tabIndex].order; newTabs[tabIndex].order = temp; setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order)); // hasUnsavedChanges는 computed value이므로 자동 계산됨 }; const moveAttributeTabDown = (tabId: string) => { const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId); if (tabIndex >= attributeSubTabs.length - 1) return; const newTabs = [...attributeSubTabs]; const temp = newTabs[tabIndex + 1].order; newTabs[tabIndex + 1].order = newTabs[tabIndex].order; newTabs[tabIndex].order = temp; setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order)); // hasUnsavedChanges는 computed value이므로 자동 계산됨 }; const getTabIcon = (iconName: string) => { const icons: Record = { FolderTree, ListTree, FileText, Settings, Layers, Database, Plus, Folder }; return icons[iconName] || FileText; }; const moveTabUp = (tabId: string) => { const tabIndex = customTabs.findIndex(t => t.id === tabId); if (tabIndex <= 0) return; const newTabs = [...customTabs]; const temp = newTabs[tabIndex - 1].order; newTabs[tabIndex - 1].order = newTabs[tabIndex].order; newTabs[tabIndex].order = temp; setCustomTabs(newTabs.sort((a, b) => a.order - b.order)); // hasUnsavedChanges는 computed value이므로 자동 계산됨 toast.success('탭 순서가 변경되었습니다'); }; const moveTabDown = (tabId: string) => { const tabIndex = customTabs.findIndex(t => t.id === tabId); if (tabIndex >= customTabs.length - 1) return; const newTabs = [...customTabs]; const temp = newTabs[tabIndex + 1].order; newTabs[tabIndex + 1].order = newTabs[tabIndex].order; newTabs[tabIndex].order = temp; setCustomTabs(newTabs.sort((a, b) => a.order - b.order)); // hasUnsavedChanges는 computed value이므로 자동 계산됨 toast.success('탭 순서가 변경되었습니다'); }; const handleEditTabFromManage = (tab: typeof customTabs[0]) => { if (tab.isDefault) return toast.error('기본 탭은 수정할 수 없습니다'); setEditingTabId(tab.id); setNewTabLabel(tab.label); setIsManageTabsDialogOpen(false); setIsAddTabDialogOpen(true); }; // 현재 섹션의 모든 필드 가져오기 (조건부 필드 참조용) const _getAllFieldsInSection = (sectionId: number) => { if (!selectedPage) return []; const section = selectedPage.sections.find(s => s.id === sectionId); return section?.fields || []; }; // 섹션 순서 변경 핸들러 (드래그앤드롭) const moveSection = (dragIndex: number, hoverIndex: number) => { if (!selectedPage) return; const sections = [...selectedPage.sections]; const [draggedSection] = sections.splice(dragIndex, 1); sections.splice(hoverIndex, 0, draggedSection); // order 값 재설정 const updatedSections = sections.map((section, idx) => ({ ...section, order: idx + 1 })); // 페이지 업데이트 updateItemPage(selectedPage.id, { sections: updatedSections }); // hasUnsavedChanges는 computed value이므로 자동 계산됨 toast.success('섹션 순서가 변경되었습니다 (저장 필요)'); }; // 필드 순서 변경 핸들러 const moveField = (sectionId: number, dragIndex: number, hoverIndex: number) => { if (!selectedPage) return; const section = selectedPage.sections.find(s => s.id === sectionId); if (!section || !section.fields) return; const newFields = [...section.fields]; const [draggedField] = newFields.splice(dragIndex, 1); newFields.splice(hoverIndex, 0, draggedField); reorderFields(sectionId, newFields.map(f => f.id)); // hasUnsavedChanges는 computed value이므로 자동 계산됨 }; // 전체 데이터 초기화 핸들러 const _handleResetAllData = () => { if (!confirm('⚠️ 경고: 모든 품목기준관리 데이터(계층구조, 섹션, 항목, 속성)를 초기화하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!')) { return; } try { // ItemMasterContext의 모든 데이터 및 캐시 초기화 resetAllData(); // 로컬 상태 초기화 (ItemMasterContext가 관리하지 않는 컴포넌트 로컬 상태) setUnitOptions([]); setMaterialOptions([]); setSurfaceTreatmentOptions([]); setCustomAttributeOptions({}); setAttributeColumns({}); setBomItems([]); // 탭 상태 초기화 (기본 탭만 남김) setCustomTabs([ { id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 }, { id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 }, { id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 }, { id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 } ]); setAttributeSubTabs([]); console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다'); toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.'); // 페이지 새로고침하여 완전히 초기화된 상태 반영 setTimeout(() => { window.location.reload(); }, 1500); } catch (error) { toast.error('초기화 중 오류가 발생했습니다'); console.error('Reset error:', error); } }; // 초기 로딩 중 UI if (isInitialLoading) { return (
); } // 에러 발생 시 UI if (error) { return (
window.location.reload()} />
); } return (
{customTabs.sort((a, b) => a.order - b.order).map(tab => { const Icon = getTabIcon(tab.icon); return ( {tab.label} ); })} {/* setIsManageTabsDialogOpen(true)}*/} {/*>*/} {/* */} {/* 탭 관리*/} {/**/} {/* 전체 초기화 버튼 숨김 처리 - 디자인에 없는 기능 */} {/* */}
{/* 속성 탭 (단위/재질/표면처리 통합) */} 속성 관리 단위, 재질, 표면처리 등의 속성을 관리합니다 {/* 속성 하위 탭 (칩 형태) */}
{attributeSubTabs.sort((a, b) => a.order - b.order).map(tab => ( ))}
{/* 단위 관리 */} {activeAttributeTab === 'units' && (

단위 목록

{unitOptions.map((option) => { const columns = attributeColumns['units'] || []; const hasColumns = columns.length > 0 && option.columnValues; const inputTypeLabel = option.inputType === 'textbox' ? '텍스트박스' : option.inputType === 'number' ? '숫자' : option.inputType === 'dropdown' ? '드롭다운' : option.inputType === 'checkbox' ? '체크박스' : option.inputType === 'date' ? '날짜' : option.inputType === 'textarea' ? '텍스트영역' : '텍스트박스'; return (
{option.label} {option.inputType && ( {inputTypeLabel} )} {option.required && ( 필수 )}
값(Value): {option.value}
{option.placeholder && (
플레이스홀더: {option.placeholder}
)} {option.defaultValue && (
기본값: {option.defaultValue}
)} {option.inputType === 'dropdown' && option.options && (
옵션:
{option.options.map((opt, idx) => ( {opt} ))}
)}
{hasColumns && (

추가 칼럼

{columns.map((column) => (
{column.name}: {option.columnValues?.[column.key] || '-'}
))}
)}
); })}
)} {/* 재질 관리 */} {activeAttributeTab === 'materials' && (

재질 목록

{materialOptions.map((option) => { const columns = attributeColumns['materials'] || []; const hasColumns = columns.length > 0 && option.columnValues; const inputTypeLabel = option.inputType === 'textbox' ? '텍스트박스' : option.inputType === 'number' ? '숫자' : option.inputType === 'dropdown' ? '드롭다운' : option.inputType === 'checkbox' ? '체크박스' : option.inputType === 'date' ? '날짜' : option.inputType === 'textarea' ? '텍스트영역' : '텍스트박스'; return (
{option.label} {option.inputType && ( {inputTypeLabel} )} {option.required && ( 필수 )}
값(Value): {option.value}
{option.placeholder && (
플레이스홀더: {option.placeholder}
)} {option.defaultValue && (
기본값: {option.defaultValue}
)} {option.inputType === 'dropdown' && option.options && (
옵션:
{option.options.map((opt, idx) => ( {opt} ))}
)}
{hasColumns && (

추가 칼럼

{columns.map((column) => (
{column.name}: {option.columnValues?.[column.key] || '-'}
))}
)}
); })}
)} {/* 표면처리 관리 */} {activeAttributeTab === 'surface' && (

표면처리 목록

{surfaceTreatmentOptions.map((option) => { const columns = attributeColumns['surface'] || []; const hasColumns = columns.length > 0 && option.columnValues; const inputTypeLabel = option.inputType === 'textbox' ? '텍스트박스' : option.inputType === 'number' ? '숫자' : option.inputType === 'dropdown' ? '드롭다운' : option.inputType === 'checkbox' ? '체크박스' : option.inputType === 'date' ? '날짜' : option.inputType === 'textarea' ? '텍스트영역' : '텍스트박스'; return (
{option.label} {option.inputType && ( {inputTypeLabel} )} {option.required && ( 필수 )}
값(Value): {option.value}
{option.placeholder && (
플레이스홀더: {option.placeholder}
)} {option.defaultValue && (
기본값: {option.defaultValue}
)} {option.inputType === 'dropdown' && option.options && (
옵션:
{option.options.map((opt, idx) => ( {opt} ))}
)}
{hasColumns && (

추가 칼럼

{columns.map((column) => (
{column.name}: {option.columnValues?.[column.key] || '-'}
))}
)}
); })}
)} {/* 사용자 정의 속성 탭 및 마스터 항목 탭 */} {!['units', 'materials', 'surface'].includes(activeAttributeTab) && (() => { const currentTabKey = activeAttributeTab; // 마스터 항목인지 확인 const masterField = itemMasterFields.find(f => f.id.toString() === currentTabKey); // 마스터 항목이면 해당 항목의 속성값들을 표시 // Note: default_properties is Record, not an array, so this condition will always be false // This code block may need refactoring to work with the actual data structure if (masterField && masterField.default_properties && Array.isArray(masterField.default_properties)) { return (

{masterField.field_name} 속성 목록

항목 탭에서 추가한 "{masterField.field_name}" 항목의 속성값들입니다

{(masterField.default_properties as any[]).map((property: any) => { const inputTypeLabel = property.type === 'textbox' ? '텍스트박스' : property.type === 'number' ? '숫자' : property.type === 'dropdown' ? '드롭다운' : property.type === 'checkbox' ? '체크박스' : property.type === 'date' ? '날짜' : property.type === 'textarea' ? '텍스트영역' : '텍스트박스'; return (
{property.label} {inputTypeLabel} {property.required && ( 필수 )}
키(Key): {property.key}
{property.placeholder && (
플레이스홀더: {property.placeholder}
)} {property.defaultValue && (
기본값: {property.defaultValue}
)} {property.type === 'dropdown' && property.options && (
옵션:
{property.options.map((opt: string, idx: number) => ( {opt} ))}
)}
); })}

마스터 항목 속성 관리

이 속성들은 항목 탭에서 "{masterField.field_name}" 항목을 편집하여 추가/수정/삭제할 수 있습니다.

); } // 사용자 정의 속성 탭 (기존 로직) const currentOptions = customAttributeOptions[currentTabKey] || []; return (

{attributeSubTabs.find(t => t.key === activeAttributeTab)?.label || '사용자 정의'} 목록

{currentOptions.length > 0 ? (
{currentOptions.map((option) => { const columns = attributeColumns[currentTabKey] || []; const hasColumns = columns.length > 0 && option.columnValues; const inputTypeLabel = option.inputType === 'textbox' ? '텍스트박스' : option.inputType === 'number' ? '숫자' : option.inputType === 'dropdown' ? '드롭다운' : option.inputType === 'checkbox' ? '체크박스' : option.inputType === 'date' ? '날짜' : option.inputType === 'textarea' ? '텍스트영역' : '텍스트박스'; return (
{option.label} {inputTypeLabel} {option.required && ( 필수 )}
값(Value): {option.value}
{option.placeholder && (
플레이스홀더: {option.placeholder}
)} {option.defaultValue && (
기본값: {option.defaultValue}
)} {option.inputType === 'dropdown' && option.options && (
옵션:
{option.options.map((opt, idx) => ( {opt} ))}
)}
{hasColumns && (

추가 칼럼

{columns.map((column) => (
{column.name}: {option.columnValues?.[column.key] || '-'}
))}
)}
); })}
) : (

아직 추가된 항목이 없습니다

위 "추가" 버튼을 클릭하여 새로운 속성을 추가할 수 있습니다

)}
); })()}
{/* 항목 탭 */} {/* 섹션관리 탭 */} {/* 계층구조 탭 */} {}} deleteItemPage={handleDeletePageWithTracking} duplicatePage={handleDuplicatePage} setIsPageDialogOpen={setIsPageDialogOpen} setIsSectionDialogOpen={setIsSectionDialogOpen} setIsFieldDialogOpen={setIsFieldDialogOpen} handleEditSectionTitle={handleEditSectionTitle} handleSaveSectionTitle={handleSaveSectionTitle} moveSection={moveSection} deleteSection={handleDeleteSectionWithTracking} updateSection={updateSection} deleteField={handleDeleteFieldWithTracking} handleEditField={handleEditField} moveField={moveField} /> {/* 사용자 정의 탭들 */} {customTabs.filter(tab => !tab.isDefault).map(tab => ( {tab.label} 사용자 정의 탭입니다. 여기에 필요한 콘텐츠를 추가할 수 있습니다.

{tab.label} 탭의 콘텐츠가 비어있습니다

이 탭에 필요한 기능을 추가하여 사용하세요

))}
{}} /> {/* 항목 추가/수정 다이얼로그 - 데스크톱 */} {!isMobile && ( s.id === selectedSectionForField) || null} selectedPage={selectedPage || null} itemMasterFields={itemMasterFields} handleAddField={handleAddField} setIsColumnDialogOpen={setIsColumnDialogOpen} setEditingColumnId={setEditingColumnId} setColumnName={setColumnName} setColumnKey={setColumnKey} /> )} {/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */} {isMobile && ( s.id === selectedSectionForField) || null} selectedPage={selectedPage || null} itemMasterFields={itemMasterFields} handleAddField={handleAddField} setIsColumnDialogOpen={setIsColumnDialogOpen} setEditingColumnId={setEditingColumnId} setColumnName={setColumnName} setColumnKey={setColumnKey} /> )} {/* 텍스트박스 컬럼 추가/수정 다이얼로그 */}
); }