'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 } from '@/contexts/ItemMasterContext'; import { BOMManagementSection, BOMItem } from '@/components/items/BOMManagementSection'; import { CategoryTab, MasterFieldTab, HierarchyTab } from './ItemMasterDataManagement/tabs'; import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog'; 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 { Database, Plus, Trash2, ChevronDown, FolderTree, Folder, FileText, Settings, ListTree, Save, X, GripVertical, Edit, Check, Package, Layers, ChevronUp } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { toast } from 'sonner'; // 품목분류 로컬스토리지 키 const ITEM_CATEGORIES_KEY = 'item-categories'; const UNIT_OPTIONS_KEY = 'unit-options'; const MATERIAL_OPTIONS_KEY = 'material-options'; const SURFACE_TREATMENT_OPTIONS_KEY = 'surface-treatment-options'; const CUSTOM_ATTRIBUTE_OPTIONS_KEY = 'custom-attribute-options'; // 품목분류 타입 interface ItemCategoryStructure { [category1: string]: { [category2: string]: string[]; }; } // 옵션 칼럼 타입 interface OptionColumn { id: string; name: string; key: string; type: 'text' | 'number'; required: boolean; } // 옵션 타입 (확장된 입력방식 지원) interface MasterOption { id: string; value: string; label: string; isActive: boolean; // 입력 방식 및 속성 inputType?: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'; required?: boolean; options?: string[]; // dropdown일 경우 선택 옵션 defaultValue?: string | number | boolean; placeholder?: string; // 기존 칼럼 시스템 (호환성 유지) columns?: OptionColumn[]; // 칼럼 정의 columnValues?: Record; // 칼럼별 값 } // 초기 데이터 const INITIAL_ITEM_CATEGORIES: ItemCategoryStructure = { "본체부품": { "가이드시스템": ["가이드레일"], "케이스시스템": ["케이스 전면부", "케이스 접검구"], }, }; const INITIAL_UNIT_OPTIONS: MasterOption[] = [ { id: 'unit-1', value: 'EA', label: 'EA (개)', isActive: true }, { id: 'unit-2', value: 'SET', label: 'SET (세트)', isActive: true }, ]; const INITIAL_MATERIAL_OPTIONS: MasterOption[] = [ { id: 'mat-1', value: 'EGI 1.2T', label: 'EGI 1.2T', isActive: true }, { id: 'mat-2', value: 'SUS 1.2T', label: 'SUS 1.2T', isActive: true }, ]; const INITIAL_SURFACE_TREATMENT_OPTIONS: MasterOption[] = [ { id: 'surf-1', value: '무도장', label: '무도장', isActive: true }, { id: 'surf-2', value: '파우더도장', label: '파우더도장', isActive: true }, ]; 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, addItemPage, updateItemPage, deleteItemPage, addSectionToPage, updateSection, deleteSection, addFieldToSection, updateField, deleteField, reorderFields, itemMasterFields, addItemMasterField, updateItemMasterField, deleteItemMasterField, sectionTemplates, addSectionTemplate, updateSectionTemplate, deleteSectionTemplate } = 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); }, []); // 동적 탭 관리 - 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 }, { id: 'categories', label: '품목분류', icon: 'Folder', isDefault: false, order: 5 } ]; }); // 마운트 후 localStorage에서 탭 로드 useEffect(() => { if (mounted && typeof window !== 'undefined') { const saved = localStorage.getItem('mes-itemMasterTabs'); if (saved) { try { const tabs = JSON.parse(saved); if (tabs && tabs.length > 0) { // 품목분류 탭이 없으면 추가 if (!tabs.find((t: any) => t.id === 'categories')) { tabs.push({ id: 'categories', label: '품목분류', icon: 'Folder', isDefault: false, order: 5 }); } // 중복 제거 const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.id === tab.id) ); setCustomTabs(uniqueTabs); } } catch { // 파싱 실패 시 기본값 유지 } } } }, [mounted]); // customTabs 변경 시 localStorage에 저장 useEffect(() => { if (mounted && typeof window !== 'undefined') { const uniqueTabs = customTabs.filter((tab, index, self) => index === self.findIndex(t => t.id === tab.id) ); localStorage.setItem('mes-itemMasterTabs', JSON.stringify(uniqueTabs)); } }, [customTabs, mounted]); const [activeTab, setActiveTab] = useState('hierarchy'); // 속성 하위 탭 관리 const [attributeSubTabs, setAttributeSubTabs] = useState>(() => { // SSR 호환: 서버 환경에서는 기본값 반환 if (typeof window === 'undefined') { return [ { 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 } ]; } const saved = localStorage.getItem('mes-attributeSubTabs'); let tabs = []; if (saved) { try { tabs = JSON.parse(saved); } catch { tabs = []; } } // 기본값이 없으면 설정 if (!tabs || tabs.length === 0) { tabs = [ { 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 } ]; } // 중복 제거 (key 기준 - 실제 데이터 의미 기준) const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.key === tab.key) ); return uniqueTabs; }); useEffect(() => { // 저장 전에도 중복 제거 (key 기준 - 실제 데이터 의미 기준) const uniqueTabs = attributeSubTabs.filter((tab, index, self) => index === self.findIndex(t => t.key === tab.key) ); localStorage.setItem('mes-attributeSubTabs', JSON.stringify(uniqueTabs)); }, [attributeSubTabs]); // 마스터 항목이 추가/수정될 때 속성 탭 자동 생성 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.fieldKey); if (!existingTab) { // 새로운 탭 추가 대상 const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), ...newTabs.map(t => t.order), -1); const newTab = { id: `attr-${field.fieldKey}`, label: field.name, key: field.fieldKey, isDefault: false, order: maxOrder + 1 }; newTabs.push(newTab); } else if (existingTab.label !== field.name) { // 이름이 변경된 경우 updatedTabs.push({ ...existingTab, label: 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]); // 컴포넌트 마운트 시 localStorage 중복 데이터 정리 (한 번만 실행) useEffect(() => { const cleanupLocalStorage = () => { // mes-attributeSubTabs 정리 const savedAttrTabs = localStorage.getItem('mes-attributeSubTabs'); if (savedAttrTabs) { try { const tabs = JSON.parse(savedAttrTabs); const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.key === tab.key) ); if (uniqueTabs.length !== tabs.length) { console.log('🧹 localStorage 중복 제거:', tabs.length - uniqueTabs.length, '개 항목 제거됨'); localStorage.setItem('mes-attributeSubTabs', JSON.stringify(uniqueTabs)); // 상태도 업데이트 setAttributeSubTabs(uniqueTabs); } } catch (error) { console.error('localStorage 정리 중 에러:', error); } } // mes-itemMasterTabs 정리 const savedMainTabs = localStorage.getItem('mes-itemMasterTabs'); if (savedMainTabs) { try { const tabs = JSON.parse(savedMainTabs); const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.id === tab.id) ); if (uniqueTabs.length !== tabs.length) { console.log('🧹 메인 탭 중복 제거:', tabs.length - uniqueTabs.length, '개 항목 제거됨'); localStorage.setItem('mes-itemMasterTabs', JSON.stringify(uniqueTabs)); setCustomTabs(uniqueTabs); } } catch (error) { console.error('메인 탭 정리 중 에러:', error); } } }; cleanupLocalStorage(); }, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행 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 [itemCategories, setItemCategories] = useState(() => { if (typeof window === 'undefined') return INITIAL_ITEM_CATEGORIES; const saved = localStorage.getItem(ITEM_CATEGORIES_KEY); return saved ? JSON.parse(saved) : INITIAL_ITEM_CATEGORIES; }); const [unitOptions, setUnitOptions] = useState(() => { if (typeof window === 'undefined') return INITIAL_UNIT_OPTIONS; const saved = localStorage.getItem(UNIT_OPTIONS_KEY); return saved ? JSON.parse(saved) : INITIAL_UNIT_OPTIONS; }); const [materialOptions, setMaterialOptions] = useState(() => { if (typeof window === 'undefined') return INITIAL_MATERIAL_OPTIONS; const saved = localStorage.getItem(MATERIAL_OPTIONS_KEY); return saved ? JSON.parse(saved) : INITIAL_MATERIAL_OPTIONS; }); const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState(() => { if (typeof window === 'undefined') return INITIAL_SURFACE_TREATMENT_OPTIONS; const saved = localStorage.getItem(SURFACE_TREATMENT_OPTIONS_KEY); return saved ? JSON.parse(saved) : INITIAL_SURFACE_TREATMENT_OPTIONS; }); // 사용자 정의 속성 옵션 상태 const [customAttributeOptions, setCustomAttributeOptions] = useState>(() => { if (typeof window === 'undefined') return {}; const saved = localStorage.getItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY); return saved ? JSON.parse(saved) : {}; }); const [newCategory1, setNewCategory1] = useState(''); const [newCategory2, setNewCategory2] = useState(''); const [newCategory3, setNewCategory3] = useState(''); const [selectedCategory1, setSelectedCategory1] = useState(''); const [selectedCategory2, setSelectedCategory2] = 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>(() => { if (typeof window === 'undefined') return {}; const saved = localStorage.getItem('attribute-columns'); return saved ? JSON.parse(saved) : {}; }); // 칼럼 추가 폼 상태 const [newColumnName, setNewColumnName] = useState(''); const [newColumnKey, setNewColumnKey] = useState(''); const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text'); const [newColumnRequired, setNewColumnRequired] = useState(false); useEffect(() => { localStorage.setItem('attribute-columns', JSON.stringify(attributeColumns)); }, [attributeColumns]); // 계층구조 상태 const [selectedPageId, setSelectedPageId] = useState(itemPages[0]?.id || null); const selectedPage = itemPages.find(p => p.id === selectedPageId) || null; const [_expandedSections, setExpandedSections] = useState>({}); const [editingSectionId, setEditingSectionId] = useState(null); const [editingSectionTitle, setEditingSectionTitle] = useState(''); // 기존 페이지들에 절대경로 자동 생성 (마이그레이션) useEffect(() => { let needsUpdate = false; itemPages.forEach(page => { if (!page.absolutePath) { const absolutePath = generateAbsolutePath(page.itemType, page.pageName); updateItemPage(page.id, { 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 [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 [pendingChanges, setPendingChanges] = useState<{ pages: { id: string; action: 'add' | 'update'; data: any }[]; sections: { id: string; action: 'add' | 'update'; data: any }[]; fields: { id: string; action: 'add' | 'update'; data: any }[]; masterFields: { id: string; action: 'add' | 'update'; data: any }[]; attributes: { id: string; action: 'add' | 'update'; type: string; data: any }[]; }>({ pages: [], sections: [], fields: [], masterFields: [], attributes: [] }); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // BOM 관리 상태 const [bomItems, setBomItems] = useState(() => { if (typeof window === 'undefined') return []; const saved = localStorage.getItem('bom-items'); return saved ? JSON.parse(saved) : []; }); // BOM 데이터 저장 useEffect(() => { localStorage.setItem('bom-items', JSON.stringify(bomItems)); }, [bomItems]); useEffect(() => { localStorage.setItem(ITEM_CATEGORIES_KEY, JSON.stringify(itemCategories)); }, [itemCategories]); useEffect(() => { localStorage.setItem(UNIT_OPTIONS_KEY, JSON.stringify(unitOptions)); }, [unitOptions]); useEffect(() => { localStorage.setItem(MATERIAL_OPTIONS_KEY, JSON.stringify(materialOptions)); }, [materialOptions]); useEffect(() => { localStorage.setItem(SURFACE_TREATMENT_OPTIONS_KEY, JSON.stringify(surfaceTreatmentOptions)); }, [surfaceTreatmentOptions]); useEffect(() => { localStorage.setItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY, JSON.stringify(customAttributeOptions)); }, [customAttributeOptions]); // 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트 useEffect(() => { itemMasterFields.forEach(field => { const attributeType = (field.property as any).attributeType; if (attributeType && attributeType !== 'custom' && field.property.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.property.options || []; const optionsChanged = JSON.stringify(currentOptions.sort()) !== JSON.stringify(newOptions.sort()); if (optionsChanged && newOptions.length > 0) { updateItemMasterField(field.id, { ...field, property: { ...field.property, options: newOptions } }); } } }); }, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions, itemMasterFields]); const _handleAddCategory1 = () => { if (!newCategory1.trim()) return toast.error('대분류명을 입력해주세요'); if (itemCategories[newCategory1]) return toast.error('이미 존재하는 대분류입니다'); setItemCategories({ ...itemCategories, [newCategory1]: {} }); setNewCategory1(''); toast.success('대분류가 추가되었습니다'); }; const _handleAddCategory2 = () => { if (!selectedCategory1) return toast.error('대분류를 선택해주세요'); if (!newCategory2.trim()) return toast.error('중분류명을 입력해주세요'); setItemCategories({ ...itemCategories, [selectedCategory1]: { ...itemCategories[selectedCategory1], [newCategory2]: [] } }); setNewCategory2(''); toast.success('중분류가 추가되었습니다'); }; const _handleAddCategory3 = () => { if (!selectedCategory1 || !selectedCategory2 || !newCategory3.trim()) return toast.error('모든 항목을 입력해주세요'); setItemCategories({ ...itemCategories, [selectedCategory1]: { ...itemCategories[selectedCategory1], [selectedCategory2]: [...itemCategories[selectedCategory1][selectedCategory2], newCategory3] } }); setNewCategory3(''); toast.success('소분류가 추가되었습니다'); }; const _handleDeleteCategory1 = (cat1: string) => { const newCategories = { ...itemCategories }; delete newCategories[cat1]; setItemCategories(newCategories); toast.success('삭제되었습니다'); }; const _handleDeleteCategory2 = (cat1: string, cat2: string) => { const newCategories = { ...itemCategories }; delete newCategories[cat1][cat2]; setItemCategories(newCategories); toast.success('삭제되었습니다'); }; const _handleDeleteCategory3 = (cat1: string, cat2: string, cat3: string) => { const newCategories = { ...itemCategories }; newCategories[cat1][cat2] = newCategories[cat1][cat2].filter(c => c !== cat3); setItemCategories(newCategories); toast.success('삭제되었습니다'); }; 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] })); } trackChange('attributes', newOption.id, 'add', newOption, editingOptionType); 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 generateAbsolutePath = (itemType: string, pageName: string): string => { const typeMap: Record = { 'FG': '제품관리', 'PT': '부품관리', 'SM': '부자재관리', 'RM': '원자재관리', 'CS': '소모품관리' }; const category = typeMap[itemType] || '기타'; return `/${category}/${pageName}`; }; // 계층구조 핸들러 const handleAddPage = () => { if (!newPageName.trim()) return toast.error('섹션명을 입력해주세요'); const absolutePath = generateAbsolutePath(newPageItemType, newPageName); const newPage: ItemPage = { id: `PAGE-${Date.now()}`, pageName: newPageName, itemType: newPageItemType, sections: [], isActive: true, absolutePath, createdAt: new Date().toISOString().split('T')[0] }; addItemPage(newPage); trackChange('pages', newPage.id, 'add', newPage); setSelectedPageId(newPage.id); setNewPageName(''); setIsPageDialogOpen(false); toast.success('페이지가 추가되었습니다 (저장 필요)'); }; const handleAddSection = () => { if (!selectedPage || !newSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요'); const newSection: ItemSection = { id: `SECTION-${Date.now()}`, title: newSectionTitle, description: newSectionDescription || undefined, fields: [], type: newSectionType, bomItems: newSectionType === 'bom' ? [] : undefined, order: selectedPage.sections.length + 1, isCollapsible: true, isCollapsed: false, createdAt: new Date().toISOString().split('T')[0] }; console.log('Adding section to page:', { pageId: selectedPage.id, pageName: selectedPage.pageName, sectionTitle: newSection.title, sectionType: newSection.type, currentSectionCount: selectedPage.sections.length, newSection: newSection }); // 1. 페이지에 섹션 추가 addSectionToPage(selectedPage.id, newSection); trackChange('sections', newSection.id, 'add', newSection); // 2. 섹션관리 탭에도 템플릿으로 자동 추가 const newTemplate: SectionTemplate = { id: `TEMPLATE-${Date.now()}`, title: newSection.title, description: newSection.description, category: [selectedPage.itemType], // 현재 페이지의 품목유형을 카테고리로 설정 fields: [], // 초기에는 빈 필드 배열 type: newSection.type, bomItems: newSection.type === 'bom' ? [] : undefined, isCollapsible: true, isCollapsed: false, isActive: true, createdAt: new Date().toISOString().split('T')[0] }; addSectionTemplate(newTemplate); trackChange('sections', newTemplate.id, 'add', newTemplate); console.log('Section added to both page and template:', { sectionId: newSection.id, templateId: newTemplate.id }); setNewSectionTitle(''); setNewSectionDescription(''); setNewSectionType('fields'); setIsSectionDialogOpen(false); toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`); }; const handleEditSectionTitle = (sectionId: string, currentTitle: string) => { setEditingSectionId(sectionId); setEditingSectionTitle(currentTitle); }; const handleSaveSectionTitle = () => { if (!selectedPage || !editingSectionId || !editingSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요'); updateSection(selectedPage.id, editingSectionId, { title: editingSectionTitle }); trackChange('sections', editingSectionId, 'update', { title: editingSectionTitle }); setEditingSectionId(null); toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)'); }; const _handleMoveSectionUp = (sectionId: string) => { 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 }); trackChange('pages', selectedPage.id, 'update', { sections: updatedSections }); toast.success('섹션 순서가 변경되었습니다'); }; const _handleMoveSectionDown = (sectionId: string) => { 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 }); trackChange('pages', selectedPage.id, 'update', { 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; const newField: ItemField = { id: editingFieldId || `FIELD-${Date.now()}`, name: newFieldName, fieldKey: newFieldKey, property: { inputType: newFieldInputType, required: newFieldRequired, row: 1, col: 1, options: newFieldInputType === 'dropdown' && newFieldOptions.trim() ? newFieldOptions.split(',').map(o => o.trim()) : undefined, multiColumn: hasColumns, columnCount: hasColumns ? textboxColumns.length : undefined, columnNames: hasColumns ? textboxColumns.map(c => c.name) : undefined }, description: newFieldDescription || undefined, displayCondition, createdAt: new Date().toISOString().split('T')[0] }; if (editingFieldId) { console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.name }); updateField(selectedPage.id, selectedSectionForField, editingFieldId, newField); trackChange('fields', editingFieldId, 'update', newField); // 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면) const existingMasterField = itemMasterFields.find(mf => mf.fieldKey === newField.fieldKey); if (existingMasterField) { const updatedMasterField: ItemMasterField = { ...existingMasterField, name: newField.name, description: newField.description, property: newField.property, displayCondition: newField.displayCondition, updatedAt: new Date().toISOString().split('T')[0] }; updateItemMasterField(existingMasterField.id, updatedMasterField); trackChange('masterFields', existingMasterField.id, 'update', updatedMasterField); } toast.success('항목이 섹션에 수정되었습니다!'); } else { console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.name, fieldKey: newField.fieldKey }); // 1. 섹션에 항목 추가 addFieldToSection(selectedPage.id, selectedSectionForField, newField); trackChange('fields', newField.id, 'add', newField); // 2. 항목관리 탭에도 마스터 항목으로 자동 추가 (중복 체크) const existingMasterField = itemMasterFields.find(mf => mf.fieldKey === newField.fieldKey); if (!existingMasterField) { const newMasterField: ItemMasterField = { id: `MASTER-FIELD-${Date.now()}`, name: newField.name, fieldKey: newField.fieldKey, description: newField.description, property: newField.property, category: [selectedPage.itemType], // 현재 페이지의 품목유형을 카테고리로 설정 displayCondition: newField.displayCondition, isActive: true, usageCount: 1, createdAt: new Date().toISOString().split('T')[0] }; addItemMasterField(newMasterField); trackChange('masterFields', newMasterField.id, 'add', newMasterField); console.log('Field added to both section and master fields:', { fieldId: newField.id, masterFieldId: newMasterField.id, fieldKey: newField.fieldKey }); // 3. dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가 if (newField.property.inputType === 'dropdown' && newField.property.options && newField.property.options.length > 0) { const existingCustomOptions = customAttributeOptions[newField.fieldKey]; if (!existingCustomOptions || existingCustomOptions.length === 0) { const customOptions = newField.property.options.map((option, index) => ({ id: `CUSTOM-${newField.fieldKey}-${Date.now()}-${index}`, value: option, label: option, isActive: true })); setCustomAttributeOptions(prev => ({ ...prev, [newField.fieldKey]: customOptions })); // 속성관리 탭에 하위 탭으로 추가 const existingTab = attributeSubTabs.find(tab => tab.key === newField.fieldKey); if (!existingTab) { const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1); const newTab = { id: `attr-${newField.fieldKey}`, label: newField.name, key: newField.fieldKey, 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.fieldKey, options: customOptions }); toast.success(`항목이 추가되고 "${newField.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(sectionId); setEditingFieldId(field.id); setNewFieldName(field.name); setNewFieldKey(field.fieldKey); setNewFieldInputType(field.property.inputType); setNewFieldRequired(field.property.required); setNewFieldOptions(field.property.options?.join(', ') || ''); setNewFieldDescription(field.description || ''); // 조건부 표시 설정 로드 if (field.displayCondition) { setNewFieldConditionEnabled(true); setNewFieldConditionTargetType(field.displayCondition.targetType); setNewFieldConditionFields(field.displayCondition.fieldConditions || []); setNewFieldConditionSections(field.displayCondition.sectionIds || []); } else { setNewFieldConditionEnabled(false); setNewFieldConditionTargetType('field'); setNewFieldConditionFields([]); setNewFieldConditionSections([]); } setIsFieldDialogOpen(true); }; // 마스터 필드 선택 시 폼 자동 채우기 useEffect(() => { if (fieldInputMode === 'master' && selectedMasterFieldId) { const masterField = itemMasterFields.find(f => f.id === selectedMasterFieldId); if (masterField) { setNewFieldName(masterField.name); setNewFieldKey(masterField.fieldKey); setNewFieldInputType(masterField.property.inputType); setNewFieldRequired(masterField.property.required); setNewFieldOptions(masterField.property.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 })); } const newMasterField: ItemMasterField = { id: `MASTER-${Date.now()}`, name: newMasterFieldName, fieldKey: newMasterFieldKey, property: { 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 } as any, properties: properties.length > 0 ? properties : undefined, category: newMasterFieldCategory, description: newMasterFieldDescription || undefined, isActive: true, createdAt: new Date().toISOString().split('T')[0] }; addItemMasterField(newMasterField); trackChange('masterFields', newMasterField.id, 'add', 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.name); setNewMasterFieldKey(field.fieldKey); setNewMasterFieldInputType(field.property.inputType); setNewMasterFieldRequired(field.property.required); setNewMasterFieldCategory(field.category || '공통'); setNewMasterFieldDescription(field.description || ''); setNewMasterFieldOptions(field.property.options?.join(', ') || ''); setNewMasterFieldAttributeType((field.property as any).attributeType || 'custom'); setNewMasterFieldMultiColumn(field.property.multiColumn || false); setNewMasterFieldColumnCount(field.property.columnCount || 2); setNewMasterFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']); setIsMasterFieldDialogOpen(true); }; const handleUpdateMasterField = () => { if (!editingMasterFieldId || !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 })); } updateItemMasterField(editingMasterFieldId, { name: newMasterFieldName, fieldKey: newMasterFieldKey, property: { 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 } as any, properties: properties.length > 0 ? properties : 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); trackChange('masterFields', editingMasterFieldId, 'update', { id: editingMasterFieldId }); toast.success('마스터 항목이 수정되었습니다 (속성 탭에 반영됨, 저장 필요)'); }; const handleDeleteMasterField = (id: string) => { if (confirm('이 마스터 항목을 삭제하시겠습니까?')) { // 삭제할 마스터 항목 찾기 const fieldToDelete = itemMasterFields.find(f => f.id === id); // 마스터 항목 삭제 deleteItemMasterField(id); // 속성 탭에서 해당 탭 제거 if (fieldToDelete) { setAttributeSubTabs(prev => prev.filter(tab => tab.key !== fieldToDelete.fieldKey)); // 삭제된 탭이 현재 활성 탭이면 다른 탭으로 전환 if (activeAttributeTab === fieldToDelete.fieldKey) { setActiveAttributeTab('units'); } } toast.success('마스터 항목이 삭제되었습니다'); } }; // 섹션 템플릿 핸들러 const handleAddSectionTemplate = () => { if (!newSectionTemplateTitle.trim()) return toast.error('섹션 제목을 입력해주세요'); const newTemplate: SectionTemplate = { id: `TEMPLATE-${Date.now()}`, title: newSectionTemplateTitle, description: newSectionTemplateDescription || undefined, category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined, fields: [], type: newSectionTemplateType, bomItems: newSectionTemplateType === 'bom' ? [] : undefined, isCollapsible: true, isCollapsed: false, isActive: true, createdAt: new Date().toISOString().split('T')[0] }; console.log('Adding section template:', newTemplate); addSectionTemplate(newTemplate); setNewSectionTemplateTitle(''); setNewSectionTemplateDescription(''); setNewSectionTemplateCategory([]); setNewSectionTemplateType('fields'); setIsSectionTemplateDialogOpen(false); toast.success('섹션 템플릿이 추가되었습니다! (템플릿 목록에서 확인 가능)'); }; const handleEditSectionTemplate = (template: SectionTemplate) => { setEditingSectionTemplateId(template.id); setNewSectionTemplateTitle(template.title); setNewSectionTemplateDescription(template.description || ''); setNewSectionTemplateCategory(template.category || []); setNewSectionTemplateType(template.type || 'fields'); setIsSectionTemplateDialogOpen(true); }; const handleUpdateSectionTemplate = () => { if (!editingSectionTemplateId || !newSectionTemplateTitle.trim()) return toast.error('섹션 제목을 입력해주세요'); updateSectionTemplate(editingSectionTemplateId, { title: newSectionTemplateTitle, description: newSectionTemplateDescription || undefined, category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined, type: newSectionTemplateType }); setEditingSectionTemplateId(null); setNewSectionTemplateTitle(''); setNewSectionTemplateDescription(''); setNewSectionTemplateCategory([]); setNewSectionTemplateType('fields'); setIsSectionTemplateDialogOpen(false); toast.success('섹션이 수정되었습니다'); }; const handleDeleteSectionTemplate = (id: string) => { if (confirm('이 섹션을 삭제하시겠습니까?')) { deleteSectionTemplate(id); toast.success('섹션이 삭제되었습니다'); } }; // 섹션 템플릿 불러오기 const handleLoadTemplate = () => { if (!selectedTemplateId || !selectedPage) { return toast.error('템플릿을 선택해주세요'); } const template = sectionTemplates.find(t => t.id === selectedTemplateId); if (!template) { return toast.error('템플릿을 찾을 수 없습니다'); } // 템플릿을 복사해서 섹션으로 추가 const newSection: ItemSection = { id: `SECTION-${Date.now()}`, title: template.title, description: template.description, category: template.category, fields: template.fields.map(field => ({ ...field, id: `FIELD-${Date.now()}-${Math.random()}`, createdAt: new Date().toISOString().split('T')[0] })), type: template.type, bomItems: template.type === 'bom' && template.bomItems ? template.bomItems.map(bom => ({ ...bom, id: `BOM-${Date.now()}-${Math.random()}`, createdAt: new Date().toISOString().split('T')[0] })) : undefined, order: selectedPage.sections.length + 1, isCollapsible: template.isCollapsible, isCollapsed: template.isCollapsed, createdAt: new Date().toISOString().split('T')[0] }; 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.fieldKey === templateFieldKey); if (!existingMasterField && !editingTemplateFieldId) { const newMasterField: ItemMasterField = { id: `MASTER-${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 } as any, category: '공통', description: templateFieldDescription || undefined, isActive: true, createdAt: 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('항목 탭에 자동으로 추가되었습니다'); } } const newField: ItemField = { id: editingTemplateFieldId || `FIELD-${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 }, description: templateFieldDescription || undefined, createdAt: new Date().toISOString().split('T')[0] }; let updatedFields; if (editingTemplateFieldId) { updatedFields = template.fields.map(f => f.id === editingTemplateFieldId ? newField : f); toast.success('항목이 수정되었습니다'); } else { updatedFields = [...template.fields, newField]; toast.success('항목이 추가되었습니다'); } updateSectionTemplate(currentTemplateId, { fields: updatedFields }); // 폼 초기화 setTemplateFieldName(''); setTemplateFieldKey(''); setTemplateFieldInputType('textbox'); setTemplateFieldRequired(false); setTemplateFieldOptions(''); setTemplateFieldDescription(''); setTemplateFieldMultiColumn(false); setTemplateFieldColumnCount(2); setTemplateFieldColumnNames(['컬럼1', '컬럼2']); setEditingTemplateFieldId(null); setIsTemplateFieldDialogOpen(false); }; const handleEditTemplateField = (templateId: string, field: ItemField) => { setCurrentTemplateId(templateId); setEditingTemplateFieldId(field.id); 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: string, fieldId: string) => { if (!confirm('이 항목을 삭제하시겠습니까?')) return; const template = sectionTemplates.find(t => t.id === templateId); if (!template) return; const updatedFields = template.fields.filter(f => f.id !== fieldId); updateSectionTemplate(templateId, { fields: updatedFields }); toast.success('항목이 삭제되었습니다'); }; // BOM 관리 핸들러 const _handleAddBOMItem = (item: Omit) => { const newItem: BOMItem = { ...item, id: `BOM-${Date.now()}`, createdAt: new Date().toISOString().split('T')[0] }; setBomItems(prev => [...prev, newItem]); }; const _handleUpdateBOMItem = (id: string, item: Partial) => { setBomItems(prev => prev.map(bom => bom.id === id ? { ...bom, ...item } : bom)); }; const _handleDeleteBOMItem = (id: string) => { setBomItems(prev => prev.filter(bom => bom.id !== id)); }; // 템플릿별 BOM 관리 핸들러 const handleAddBOMItemToTemplate = (templateId: string, item: Omit) => { const newItem: BOMItem = { ...item, id: `BOM-${Date.now()}`, createdAt: new Date().toISOString().split('T')[0] }; const template = sectionTemplates.find(t => t.id === templateId); if (!template) return; const updatedBomItems = [...(template.bomItems || []), newItem]; updateSectionTemplate(templateId, { bomItems: updatedBomItems }); }; const handleUpdateBOMItemInTemplate = (templateId: string, itemId: string, 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: string, itemId: string) => { 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: `TAB-${Date.now()}`, 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)); }; 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)); }; 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)); 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)); 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: string) => { 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 }); trackChange('pages', selectedPage.id, 'update', { sections: updatedSections }); toast.success('섹션 순서가 변경되었습니다 (저장 필요)'); }; // 필드 순서 변경 핸들러 const moveField = (sectionId: string, dragIndex: number, hoverIndex: number) => { if (!selectedPage) return; const section = selectedPage.sections.find(s => s.id === sectionId); if (!section) return; const newFields = [...section.fields]; const [draggedField] = newFields.splice(dragIndex, 1); newFields.splice(hoverIndex, 0, draggedField); reorderFields(selectedPage.id, sectionId, newFields.map(f => f.id)); }; // 변경사항 추적 함수 const trackChange = (type: 'pages' | 'sections' | 'fields' | 'masterFields' | 'attributes', id: string, action: 'add' | 'update', data: any, attributeType?: string) => { setPendingChanges(prev => { const updated = { ...prev }; if (type === 'attributes') { const existingIndex = updated.attributes.findIndex(item => item.id === id); if (existingIndex >= 0) { updated.attributes[existingIndex] = { id, action, type: attributeType || '', data }; } else { updated.attributes.push({ id, action, type: attributeType || '', data }); } } else { const existingIndex = updated[type].findIndex(item => item.id === id); if (existingIndex >= 0) { updated[type][existingIndex] = { id, action, data }; } else { updated[type].push({ id, action, data }); } } return updated; }); setHasUnsavedChanges(true); }; // 일괄 저장 핸들러 - 모든 페이지, 섹션, 항목, 속성을 통합 저장 const handleSaveAllChanges = () => { if (!hasUnsavedChanges) { return toast.info('저장할 변경사항이 없습니다'); } try { // 모든 변경사항은 이미 DataContext에 실시간으로 반영되어 있습니다. // DataContext의 useEffect가 자동으로 localStorage에 저장합니다. // 이 함수는 변경사항 추적을 초기화하고 사용자에게 확인 메시지를 보여줍니다. const totalChanges = pendingChanges.pages.length + pendingChanges.sections.length + pendingChanges.fields.length + pendingChanges.masterFields.length + pendingChanges.attributes.length; // 변경사항 요약 const summary = []; if (pendingChanges.pages.length > 0) summary.push(`페이지 ${pendingChanges.pages.length}개`); if (pendingChanges.sections.length > 0) summary.push(`섹션 ${pendingChanges.sections.length}개`); if (pendingChanges.fields.length > 0) summary.push(`항목 ${pendingChanges.fields.length}개`); if (pendingChanges.masterFields.length > 0) summary.push(`마스터항목 ${pendingChanges.masterFields.length}개`); if (pendingChanges.attributes.length > 0) summary.push(`속성 ${pendingChanges.attributes.length}개`); console.log('Confirming changes:', { totalChanges, pendingChanges, currentItemPages: itemPages }); // itemPages, sectionTemplates, itemMasterFields를 명시적으로 localStorage에 저장 (안전성 보장) localStorage.setItem('mes-itemPages', JSON.stringify(itemPages)); localStorage.setItem('mes-sectionTemplates', JSON.stringify(sectionTemplates)); localStorage.setItem('mes-itemMasterFields', JSON.stringify(itemMasterFields)); console.log('Saved to localStorage:', { itemPages: itemPages.length, sectionTemplates: sectionTemplates.length, itemMasterFields: itemMasterFields.length }); // 속성 탭의 모든 데이터도 localStorage에 명시적으로 저장 (안전성 보장) localStorage.setItem(UNIT_OPTIONS_KEY, JSON.stringify(unitOptions)); localStorage.setItem(MATERIAL_OPTIONS_KEY, JSON.stringify(materialOptions)); localStorage.setItem(SURFACE_TREATMENT_OPTIONS_KEY, JSON.stringify(surfaceTreatmentOptions)); localStorage.setItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY, JSON.stringify(customAttributeOptions)); localStorage.setItem(ITEM_CATEGORIES_KEY, JSON.stringify(itemCategories)); // 변경사항 초기화 setPendingChanges({ pages: [], sections: [], fields: [], masterFields: [], attributes: [] }); setHasUnsavedChanges(false); toast.success(`✅ 모든 변경사항 저장 완료!\n${summary.join(', ')} - 총 ${totalChanges}건\n페이지, 섹션, 항목, 속성이 모두 자동 목록에 반영되었습니다.`); } catch (error) { toast.error('저장 중 오류가 발생했습니다'); console.error('Save error:', error); } }; return ( {/* 전역 저장 버튼 - 모든 탭의 변경사항을 저장 */} {hasUnsavedChanges && (
{pendingChanges.pages.length + pendingChanges.sections.length + pendingChanges.fields.length + pendingChanges.masterFields.length + pendingChanges.attributes.length}개 변경사항
{pendingChanges.pages.length > 0 && • 페이지 {pendingChanges.pages.length}개} {pendingChanges.sections.length > 0 && • 섹션 {pendingChanges.sections.length}개} {pendingChanges.fields.length > 0 && • 항목 {pendingChanges.fields.length}개} {pendingChanges.masterFields.length > 0 && • 마스터항목 {pendingChanges.masterFields.length}개} {pendingChanges.attributes.length > 0 && • 속성 {pendingChanges.attributes.length}개}
)}
{customTabs.sort((a, b) => a.order - b.order).map(tab => { const Icon = getTabIcon(tab.icon); return ( {tab.label} ); })}
{/* 속성 탭 (단위/재질/표면처리 통합) */} 속성 관리 단위, 재질, 표면처리 등의 속성을 관리합니다 {/* 속성 하위 탭 (칩 형태) */}
{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.fieldKey === currentTabKey); // 마스터 항목이면 해당 항목의 속성값들을 표시 if (masterField && masterField.properties && masterField.properties.length > 0) { return (

{masterField.name} 속성 목록

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

{masterField.properties.map((property) => { 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, idx) => ( {opt} ))}
)}
); })}

마스터 항목 속성 관리

이 속성들은 항목 탭에서 "{masterField.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] || '-'}
))}
)}
); })}
) : (

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

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

)}
); })()}
{/* 항목 탭 */} {/* 섹션관리 탭 */}
섹션관리 재사용 가능한 섹션 템플릿을 관리합니다
일반 섹션 모듈 섹션 {/* 일반 섹션 탭 */} {(() => { console.log('Rendering section templates:', { totalTemplates: sectionTemplates.length, generalTemplates: sectionTemplates.filter(t => t.type !== 'bom').length, templates: sectionTemplates.map(t => ({ id: t.id, title: t.title, type: t.type })) }); return null; })()} {sectionTemplates.filter(t => t.type !== 'bom').length === 0 ? (

등록된 일반 섹션이 없습니다

섹션추가 버튼을 눌러 재사용 가능한 섹션을 등록하세요.

) : (
{sectionTemplates.filter(t => t.type !== 'bom').map((template) => (
{template.title} {template.description && ( {template.description} )}
{template.category && template.category.length > 0 && (
{template.category.map((cat, idx) => ( {ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat} ))}
)}

이 템플릿과 관련되는 항목 목록을 조회합니다

{template.fields.length === 0 ? (

항목을 활용을 구간이에만 추가 버튼을 클릭해보세요

품목의 목록명, 수량, 입력방법 고객화된 표시할 수 있습니다

) : (
{template.fields.map((field, _index) => (
{field.name} {INPUT_TYPE_OPTIONS.find(t => t.value === field.property.inputType)?.label} {field.property.required && ( 필수 )}
필드키: {field.fieldKey} {field.description && ( • {field.description} )}
))}
)}
))}
)}
{/* 모듈 섹션 (BOM) 탭 */} {sectionTemplates.filter(t => t.type === 'bom').length === 0 ? (

등록된 모듈 섹션이 없습니다

섹션추가 버튼을 눌러 BOM 모듈 섹션을 등록하세요.

) : (
{sectionTemplates.filter(t => t.type === 'bom').map((template) => (
{template.title} {template.description && ( {template.description} )}
{template.category && template.category.length > 0 && (
{template.category.map((cat, idx) => ( {ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat} ))}
)}
handleAddBOMItemToTemplate(template.id, item)} onUpdateItem={(itemId, item) => handleUpdateBOMItemInTemplate(template.id, itemId, item)} onDeleteItem={(itemId) => handleDeleteBOMItemFromTemplate(template.id, itemId)} />
))}
)}
{/* 계층구조 탭 */} {/* 품목분류 탭 */} {/* 사용자 정의 탭들 (품목분류 제외) */} {customTabs.filter(tab => !tab.isDefault && tab.id !== 'categories').map(tab => ( {tab.label} 사용자 정의 탭입니다. 여기에 필요한 콘텐츠를 추가할 수 있습니다.

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

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

))}
{/* 칼럼 관리 다이얼로그 */} 칼럼 관리 {managingColumnType === 'units' && '단위'} {managingColumnType === 'materials' && '재질'} {managingColumnType === 'surface' && '표면처리'} {managingColumnType && !['units', 'materials', 'surface'].includes(managingColumnType) && (attributeSubTabs.find(t => t.key === managingColumnType)?.label || '속성')} {' '}에 추가 칼럼을 설정합니다 (예: 규격 안에 속성/값/단위 나누기)
{/* 기존 칼럼 목록 */} {managingColumnType && attributeColumns[managingColumnType]?.length > 0 && (

설정된 칼럼

{attributeColumns[managingColumnType].map((column, idx) => (
{idx + 1}

{column.name}

키: {column.key} | 타입: {column.type === 'text' ? '텍스트' : '숫자'} {column.required && ' | 필수'}

))}
)} {/* 새 칼럼 추가 폼 */}

새 칼럼 추가

setNewColumnName(e.target.value)} placeholder="예: 속성, 값, 단위" />
setNewColumnKey(e.target.value)} placeholder="예: property, value, unit" />
{/* 절대경로 편집 다이얼로그 */} !open && setEditingPathPageId(null)}> 절대경로 수정 페이지의 절대경로를 수정합니다 (예: /제품관리/제품등록)
setEditingAbsolutePath(e.target.value)} placeholder="/제품관리/제품등록" />

슬래시(/)로 시작하며, 경로를 슬래시로 구분합니다

{/* 섹션 추가 다이얼로그 */} 섹션 추가 새로운 품목 섹션을 생성합니다
setNewPageName(e.target.value)} placeholder="예: 품목 등록" />
{/* 섹션 추가 다이얼로그 */} { setIsSectionDialogOpen(open); if (!open) setNewSectionType('fields'); }}> {newSectionType === 'bom' ? 'BOM 섹션' : '일반 섹션'} 추가 {newSectionType === 'bom' ? '새로운 BOM(자재명세서) 섹션을 추가합니다' : '새로운 일반 섹션을 추가합니다'}
setNewSectionTitle(e.target.value)} placeholder={newSectionType === 'bom' ? '예: BOM 구성' : '예: 기본 정보'} />