'use client'; import { useState, useEffect, useMemo } from 'react'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { useItemMaster } from '@/contexts/ItemMasterContext'; import type { SectionTemplate, BOMItem, TemplateField } from '@/contexts/ItemMasterContext'; import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs'; import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog'; // ConditionalFieldConfig type removed - not currently used 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 { ImportSectionDialog } from './ItemMasterDataManagement/dialogs/ImportSectionDialog'; import { ImportFieldDialog } from './ItemMasterDataManagement/dialogs/ImportFieldDialog'; import { itemMasterApi } from '@/lib/api/item-master'; import { getErrorMessage, ApiError } from '@/lib/api/error-handler'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; import { transformPagesResponse, transformSectionsResponse, transformSectionTemplatesResponse, transformFieldsResponse, transformCustomTabsResponse, transformUnitOptionsResponse, transformSectionTemplateFromSection, } from '@/lib/api/transformers'; import { Database, Plus, Trash2, FileText, Settings, Package, } 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 { usePageManagement, useSectionManagement, useFieldManagement, useMasterFieldManagement, useTemplateManagement, useAttributeManagement, useTabManagement, } from './ItemMasterDataManagement/hooks'; 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: '텍스트영역' } ]; // 입력 타입 라벨 변환 헬퍼 함수 (중복 코드 제거) const getInputTypeLabel = (inputType: string | undefined): string => { const labels: Record = { textbox: '텍스트박스', number: '숫자', dropdown: '드롭다운', checkbox: '체크박스', date: '날짜', textarea: '텍스트영역', }; return labels[inputType || ''] || '텍스트박스'; }; export function ItemMasterDataManagement() { const { itemPages, loadItemPages, updateItemPage, deleteItemPage, updateSection, deleteSection, reorderFields, itemMasterFields, loadItemMasterFields, sectionTemplates, loadSectionTemplates, resetAllData, // 2025-11-26 추가: 독립 엔티티 관리 independentSections, loadIndependentSections, loadIndependentFields, refreshIndependentSections, refreshIndependentFields, linkSectionToPage, linkFieldToSection, unlinkFieldFromSection, getSectionUsage, getFieldUsage, cloneSection, reorderSections, // 2025-11-27 추가: BOM 항목 API 함수 addBOMItem, updateBOMItem, deleteBOMItem, } = useItemMaster(); console.log('ItemMasterDataManagement: Current sectionTemplates', sectionTemplates); // 2025-11-27: itemPages 변화 추적 (디버깅용) useEffect(() => { console.log('[ItemMasterDataManagement] ⚡ itemPages changed:', { pageCount: itemPages.length, pages: itemPages.map(p => ({ id: p.id, name: p.page_name, sectionsCount: p.sections.length, sections: p.sections.map(s => ({ id: s.id, title: s.title, fieldsCount: s.fields?.length || 0 })) })) }); }, [itemPages]); // ===== 커스텀 훅 초기화 ===== const pageManagement = usePageManagement(); const sectionManagement = useSectionManagement(); const fieldManagement = useFieldManagement(); const masterFieldManagement = useMasterFieldManagement(); const templateManagement = useTemplateManagement(); const attributeManagement = useAttributeManagement(); const tabManagement = useTabManagement(); // 훅에서 필요한 값들 구조분해 const { selectedPageId, setSelectedPageId, selectedPage, editingPageId, setEditingPageId, editingPageName, setEditingPageName, isPageDialogOpen, setIsPageDialogOpen, newPageName, setNewPageName, newPageItemType, setNewPageItemType, editingPathPageId, setEditingPathPageId, editingAbsolutePath, setEditingAbsolutePath, handleAddPage, handleDuplicatePage, } = pageManagement; const { editingSectionId, setEditingSectionId, editingSectionTitle, setEditingSectionTitle, isSectionDialogOpen, setIsSectionDialogOpen, newSectionTitle, setNewSectionTitle, newSectionDescription, setNewSectionDescription, newSectionType, setNewSectionType, sectionInputMode, setSectionInputMode, selectedSectionTemplateId, setSelectedSectionTemplateId, handleAddSection, handleLinkTemplate, handleEditSectionTitle, handleSaveSectionTitle, handleUnlinkSection, } = sectionManagement; const { isFieldDialogOpen, setIsFieldDialogOpen, selectedSectionForField, setSelectedSectionForField, editingFieldId, setEditingFieldId, fieldInputMode, setFieldInputMode, showMasterFieldList, setShowMasterFieldList, selectedMasterFieldId, setSelectedMasterFieldId, newFieldName, setNewFieldName, newFieldKey, setNewFieldKey, newFieldInputType, setNewFieldInputType, newFieldRequired, setNewFieldRequired, newFieldOptions, setNewFieldOptions, newFieldDescription, setNewFieldDescription, textboxColumns, setTextboxColumns, isColumnDialogOpen, setIsColumnDialogOpen, editingColumnId, setEditingColumnId, columnName, setColumnName, columnKey, setColumnKey, newFieldConditionEnabled, setNewFieldConditionEnabled, newFieldConditionTargetType, setNewFieldConditionTargetType, newFieldConditionFields, setNewFieldConditionFields, newFieldConditionSections, setNewFieldConditionSections, tempConditionValue, setTempConditionValue, handleAddField, handleEditField, } = fieldManagement; const { isMasterFieldDialogOpen, setIsMasterFieldDialogOpen, editingMasterFieldId, setEditingMasterFieldId, newMasterFieldName, setNewMasterFieldName, newMasterFieldKey, setNewMasterFieldKey, newMasterFieldInputType, setNewMasterFieldInputType, newMasterFieldRequired, setNewMasterFieldRequired, newMasterFieldCategory, setNewMasterFieldCategory, newMasterFieldDescription, setNewMasterFieldDescription, newMasterFieldOptions, setNewMasterFieldOptions, newMasterFieldAttributeType, setNewMasterFieldAttributeType, newMasterFieldMultiColumn, setNewMasterFieldMultiColumn, newMasterFieldColumnCount, setNewMasterFieldColumnCount, newMasterFieldColumnNames, setNewMasterFieldColumnNames, handleAddMasterField, handleEditMasterField, handleUpdateMasterField, handleDeleteMasterField, } = masterFieldManagement; const { isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen, editingSectionTemplateId, setEditingSectionTemplateId, newSectionTemplateTitle, setNewSectionTemplateTitle, newSectionTemplateDescription, setNewSectionTemplateDescription, newSectionTemplateCategory, setNewSectionTemplateCategory, newSectionTemplateType, setNewSectionTemplateType, isLoadTemplateDialogOpen, setIsLoadTemplateDialogOpen, selectedTemplateId, setSelectedTemplateId, isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen, currentTemplateId: _currentTemplateId, setCurrentTemplateId, editingTemplateFieldId, setEditingTemplateFieldId, templateFieldName, setTemplateFieldName, templateFieldKey, setTemplateFieldKey, templateFieldInputType, setTemplateFieldInputType, templateFieldRequired, setTemplateFieldRequired, templateFieldOptions, setTemplateFieldOptions, templateFieldDescription, setTemplateFieldDescription, templateFieldMultiColumn, setTemplateFieldMultiColumn, templateFieldColumnCount, setTemplateFieldColumnCount, templateFieldColumnNames, setTemplateFieldColumnNames, templateFieldInputMode, setTemplateFieldInputMode, templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList, templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId, handleAddSectionTemplate, handleEditSectionTemplate, handleUpdateSectionTemplate, handleDeleteSectionTemplate, handleLoadTemplate, handleAddTemplateField, handleEditTemplateField, handleDeleteTemplateField, handleAddBOMItemToTemplate, handleUpdateBOMItemInTemplate, handleDeleteBOMItemFromTemplate, } = templateManagement; const { unitOptions, setUnitOptions, materialOptions, setMaterialOptions, surfaceTreatmentOptions, setSurfaceTreatmentOptions, customAttributeOptions, setCustomAttributeOptions, isOptionDialogOpen, setIsOptionDialogOpen, editingOptionType, setEditingOptionType, newOptionValue, setNewOptionValue, newOptionLabel, setNewOptionLabel, newOptionColumnValues, setNewOptionColumnValues, newOptionInputType, setNewOptionInputType, newOptionRequired, setNewOptionRequired, newOptionOptions, setNewOptionOptions, newOptionPlaceholder, setNewOptionPlaceholder, newOptionDefaultValue, setNewOptionDefaultValue, isColumnManageDialogOpen, setIsColumnManageDialogOpen, managingColumnType, setManagingColumnType, attributeColumns, setAttributeColumns, newColumnName, setNewColumnName, newColumnKey, setNewColumnKey, newColumnType, setNewColumnType, newColumnRequired, setNewColumnRequired, handleAddOption, handleDeleteOption, handleAddColumn: _handleAddColumn, handleDeleteColumn: _handleDeleteColumn, } = attributeManagement; const { customTabs, setCustomTabs, activeTab, setActiveTab, attributeSubTabs, setAttributeSubTabs, activeAttributeTab, setActiveAttributeTab, isAddTabDialogOpen, setIsAddTabDialogOpen, isManageTabsDialogOpen, setIsManageTabsDialogOpen, newTabLabel, setNewTabLabel, editingTabId, setEditingTabId, deletingTabId, setDeletingTabId, isDeleteTabDialogOpen, setIsDeleteTabDialogOpen, isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen, isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen, newAttributeTabLabel, setNewAttributeTabLabel, editingAttributeTabId, setEditingAttributeTabId, deletingAttributeTabId, setDeletingAttributeTabId, isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen, handleAddTab, handleUpdateTab, handleDeleteTab, confirmDeleteTab, handleAddAttributeTab, handleUpdateAttributeTab, handleDeleteAttributeTab, confirmDeleteAttributeTab, moveTabUp, moveTabDown, moveAttributeTabUp, moveAttributeTabDown, getTabIcon, handleEditTabFromManage, } = tabManagement; // 모든 페이지의 섹션을 하나의 배열로 평탄화 const _itemSections = itemPages.flatMap(page => page.sections.map(section => ({ ...section, parentPageId: page.id })) ); // 2025-11-26: itemPages의 모든 섹션 + 독립 섹션(independentSections)을 SectionTemplate 형식으로 변환 // 이렇게 하면 계층구조 탭과 섹션 탭이 같은 데이터 소스를 사용하여 자동 동기화됨 // 독립 섹션: 페이지에서 연결 해제된 섹션 (page_id = null) const sectionsAsTemplates: SectionTemplate[] = useMemo(() => { console.log('[sectionsAsTemplates] useMemo 재계산! itemPages:', itemPages.map(p => ({ id: p.id, sections: p.sections.map(s => ({ id: s.id, fieldsCount: s.fields?.length || 0 })) }))); // 1. itemPages에 연결된 섹션들 const linkedSections = itemPages.flatMap(page => page.sections.map(section => ({ id: section.id, tenant_id: section.tenant_id || 0, template_name: section.title, section_type: section.section_type, description: section.description || null, default_fields: null, // ItemField → TemplateField 변환 fields: section.fields?.map(field => ({ id: field.id.toString(), name: field.field_name, fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'), property: { inputType: field.field_type, // 2025-11-27: is_required와 properties.required 둘 다 체크 required: field.is_required || field.properties?.required, options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), }, description: field.placeholder || undefined, } as TemplateField)), bomItems: section.bom_items, created_by: section.created_by || null, updated_by: section.updated_by || null, created_at: section.created_at, updated_at: section.updated_at, })) ); // 2. 독립 섹션들 (page_id = null, 연결 해제된 섹션) const unlinkedSections = independentSections.map(section => ({ id: section.id, tenant_id: section.tenant_id || 0, template_name: section.title, section_type: section.section_type, description: section.description || null, default_fields: null, fields: section.fields?.map(field => ({ id: field.id.toString(), name: field.field_name, fieldKey: field.field_name.toLowerCase().replace(/\s+/g, '_'), property: { inputType: field.field_type, // 2025-11-27: is_required와 properties.required 둘 다 체크 required: field.is_required || field.properties?.required, options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value), }, description: field.placeholder || undefined, } as TemplateField)), bomItems: section.bom_items, created_by: section.created_by || null, updated_by: section.updated_by || null, created_at: section.created_at, updated_at: section.updated_at, })); // 3. 중복 제거 (같은 섹션이 여러 페이지에 연결되었거나, 연결 섹션과 독립 섹션에 동시 존재하는 경우) const allSections = [...linkedSections, ...unlinkedSections]; const uniqueSections = Array.from( new Map(allSections.map(s => [s.id, s])).values() ); return uniqueSections; }, [itemPages, independentSections]); // 2025-11-27: sectionsAsTemplates 변화 추적 (디버깅용) useEffect(() => { console.log('[ItemMasterDataManagement] 📋 sectionsAsTemplates changed:', { count: sectionsAsTemplates.length, sections: sectionsAsTemplates.map(s => ({ id: s.id, name: s.template_name, fieldsCount: s.fields?.length || 0 })) }); }, [sectionsAsTemplates]); // 마운트 상태 추적 (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(); // 2025-11-26: 백엔드가 entity_relationships 기반으로 변경됨 // - pages[].sections: entity_relationships 기반으로 연결된 섹션 (이미 포함) // - sections: 모든 독립 섹션 (재사용 가능 목록) // - sectionTemplates: 삭제됨 → sections로 대체 // 1. 페이지 데이터 로드 (섹션이 이미 포함되어 있음) const transformedPages = transformPagesResponse(data.pages); loadItemPages(transformedPages); // 2. 독립 섹션 로드 (모든 재사용 가능 섹션) // 백엔드가 sections 배열로 모든 독립 섹션을 반환 if (data.sections && data.sections.length > 0) { const transformedSections = transformSectionsResponse(data.sections); loadIndependentSections(transformedSections); console.log('✅ 독립 섹션 로드:', transformedSections.length); } // 3. 섹션 템플릿 로드 (sectionTemplates → sections로 통합됨) // 기존 sectionTemplates가 있으면 호환성 유지, 없으면 sections 사용 if (data.sectionTemplates && data.sectionTemplates.length > 0) { const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates); loadSectionTemplates(transformedTemplates); } else if (data.sections && data.sections.length > 0) { // sectionTemplates가 없으면 sections에서 is_template=true인 것만 사용 const templates = data.sections .filter((s: { is_template?: boolean }) => s.is_template) .map(transformSectionTemplateFromSection); if (templates.length > 0) { loadSectionTemplates(templates); } } // 필드 로드 (2025-11-27: masterFields가 fields로 통합됨) // data.fields = 모든 필드 목록 (백엔드 init API에서 반환) if (data.fields && data.fields.length > 0) { const transformedFields = transformFieldsResponse(data.fields); // 2025-11-27: section_id가 null인 필드만 필터링 (독립 필드) const independentOnlyFields = transformedFields.filter( f => f.section_id === null || f.section_id === undefined ); // 2025-11-27: 항목탭용 (itemMasterFields) - 모든 필드 로드 // 계층구조에서 추가한 필드도 항목탭에 바로 표시되도록 함 // addFieldToSection에서 setItemMasterFields를 호출하므로 일관성 유지 loadItemMasterFields(transformedFields as any); // 독립 필드용 (independentFields) - section_id=null인 필드만 loadIndependentFields(independentOnlyFields); console.log('✅ 필드 로드:', { total: transformedFields.length, independent: independentOnlyFields.length, allFieldsForItemsTab: transformedFields.length, }); } // 커스텀 탭 로드 (local state) - 교체 방식 (복제 방지) if (data.customTabs && data.customTabs.length > 0) { const transformedTabs = transformCustomTabsResponse(data.customTabs); setCustomTabs(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 || 0, sections: data.sections?.length || 0, fields: data.fields?.length || 0, 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(); }, []); // ===== 훅으로 이동된 상태들 (참조용 주석) ===== // 탭, 속성, 페이지, 섹션 관련 상태는 위의 훅에서 관리됩니다. // 모바일 체크 const [isMobile, setIsMobile] = useState(false); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 768); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // 필드, 마스터필드, 템플릿 관련 상태는 위의 훅에서 관리됩니다. // BOM 관리 상태 (훅에 없음) const [_bomItems, setBomItems] = useState([]); // 2025-11-26 추가: 섹션/필드 불러오기 다이얼로그 상태 const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false); const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false); const [selectedImportSectionId, setSelectedImportSectionId] = useState(null); const [selectedImportFieldId, setSelectedImportFieldId] = useState(null); const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState(null); // 2025-11-26 추가: 섹션 불러오기 핸들러 const handleImportSection = async () => { if (!selectedPageId || !selectedImportSectionId) return; try { await linkSectionToPage(selectedPageId, selectedImportSectionId); toast.success('섹션을 불러왔습니다.'); setSelectedImportSectionId(null); } catch (error) { console.error('섹션 불러오기 실패:', error); toast.error(getErrorMessage(error)); } }; /** * 필드 불러오기 핸들러 * * @description 2025-11-27: API 변경으로 단순화 * - 이전: source 파라미터로 'master' | 'independent' 구분 * - 현재: 모든 필드가 item_fields로 통합 → linkFieldToSection만 사용 * - section_id=NULL인 필드를 섹션에 연결하는 방식으로 통일 */ const handleImportField = async () => { if (!importFieldTargetSectionId || !selectedImportFieldId) return; try { // 2025-11-27: 통합된 필드 연결 방식 await linkFieldToSection(importFieldTargetSectionId, selectedImportFieldId); toast.success('필드를 섹션에 연결했습니다.'); setSelectedImportFieldId(null); setImportFieldTargetSectionId(null); } catch (error) { console.error('필드 불러오기 실패:', error); toast.error(getErrorMessage(error)); } }; // 2025-11-26 추가: 섹션 복제 핸들러 const handleCloneSection = async (sectionId: number) => { try { await cloneSection(sectionId); toast.success('섹션이 복제되었습니다.'); } catch (error) { console.error('섹션 복제 실패:', error); toast.error(getErrorMessage(error)); } }; // ===== 이하 핸들러들은 훅으로 이동되어 제거됨 ===== // handleAddOption, handleDeleteOption → useAttributeManagement // handleAddPage, handleDuplicatePage → usePageManagement // handleAddSection, handleLinkTemplate, handleEditSectionTitle, handleSaveSectionTitle → useSectionManagement // handleAddField, handleEditField → useFieldManagement // handleAddMasterField, handleEditMasterField, handleUpdateMasterField, handleDeleteMasterField → useMasterFieldManagement // handleAddSectionTemplate 등 템플릿 관련 → useTemplateManagement // handleAddTab 등 탭 관련 → useTabManagement // 페이지 삭제 핸들러 (pendingChanges 제거 포함) - 훅에 없어 유지 const handleDeletePageWithTracking = (pageId: number) => { const pageToDelete = itemPages.find(p => p.id === pageId); const sectionIds = pageToDelete?.sections.map(s => s.id) || []; const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || []; 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); const fieldIds = sectionToDelete?.fields?.map(f => f.id) || []; deleteSection(Number(sectionId)); console.log('섹션 삭제 완료:', { sectionId, removedFields: fieldIds.length }); }; // 필드 연결 해제 핸들러 (2025-11-27: 삭제 → unlink로 변경) // 섹션에서 필드 연결만 해제하고, 필드 자체는 독립 필드 목록에 유지됨 const handleUnlinkFieldWithTracking = async (_pageId: string, sectionId: string, fieldId: string) => { try { await unlinkFieldFromSection(Number(sectionId), Number(fieldId)); console.log('필드 연결 해제 완료:', fieldId); } catch (error) { console.error('필드 연결 해제 실패:', error); toast.error('필드 연결 해제에 실패했습니다'); } }; // 절대경로 업데이트 - 로컬에서 처리 const _handleUpdateAbsolutePathLocal = (pageId: number, newPath: string) => { updateItemPage(pageId, { absolute_path: newPath }); toast.success('절대경로가 업데이트되었습니다'); }; // 필드 순서 변경 const _handleReorderFieldsLocal = (sectionId: number, orderedFieldIds: number[]) => { reorderFields(sectionId, orderedFieldIds); }; // 페이지 이름 업데이트 const _handleUpdatePageNameLocal = (pageId: number, newName: string) => { updateItemPage(pageId, { page_name: newName }); toast.success('페이지 이름이 업데이트되었습니다'); }; // ===== 래퍼 함수들 (훅 함수에 selectedPage 바인딩 및 타입 호환성) ===== const handleAddSectionWrapper = () => handleAddSection(selectedPage); const handleLinkTemplateWrapper = (template: SectionTemplate) => handleLinkTemplate(template, selectedPage); const handleSaveSectionTitleWrapper = () => handleSaveSectionTitle(selectedPage); const handleAddFieldWrapper = () => handleAddField(selectedPage); const handleLoadTemplateWrapper = () => handleLoadTemplate(selectedPage); // setter 래퍼들 (Dispatch 타입 호환성) // eslint-disable-next-line @typescript-eslint/no-explicit-any const setNewSectionTypeWrapper: React.Dispatch> = setNewSectionType as any; // eslint-disable-next-line @typescript-eslint/no-explicit-any const setNewPageItemTypeWrapper: React.Dispatch> = setNewPageItemType as any; // ===== 유틸리티 함수들 ===== // 현재 섹션의 모든 필드 가져오기 (조건부 필드 참조용) const _getAllFieldsInSection = (sectionId: number) => { if (!selectedPage) return []; const section = selectedPage.sections.find(s => s.id === sectionId); return section?.fields || []; }; // 섹션 순서 변경 핸들러 (드래그앤드롭) const moveSection = async (dragIndex: number, hoverIndex: number) => { if (!selectedPage) return; const sections = [...selectedPage.sections]; const [draggedSection] = sections.splice(dragIndex, 1); sections.splice(hoverIndex, 0, draggedSection); // 새로운 순서의 섹션 ID 배열 생성 const sectionIds = sections.map(s => s.id); try { // API를 통해 섹션 순서 변경 (Context의 reorderSections 사용) await reorderSections(selectedPage.id, sectionIds); toast.success('섹션 순서가 변경되었습니다'); } catch (error) { console.error('섹션 순서 변경 실패:', error); toast.error('섹션 순서 변경에 실패했습니다'); } }; // 필드 순서 변경 핸들러 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()} showContactInfo={true} /> ); } 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; return (
{option.label} {option.inputType && ( {getInputTypeLabel(option.inputType)} )} {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; return (
{option.label} {option.inputType && ( {getInputTypeLabel(option.inputType)} )} {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 = getInputTypeLabel(option.inputType); 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: properties is Record | null, convert to array for display const propertiesArray = masterField?.properties ? Object.entries(masterField.properties).map(([key, value]) => ({ key, ...value as object })) : []; if (masterField && propertiesArray.length > 0) { return (

{masterField.field_name} 속성 목록

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

{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {propertiesArray.map((property: any) => { const inputTypeLabel = getInputTypeLabel(property.type); 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 = getInputTypeLabel(option.inputType); 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] || '-'}
))}
)}
); })}
) : (

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

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

)}
); })()}
{/* 항목 탭 */} {/* 섹션관리 탭 */} ({ value: opt.value, label: opt.label }))} onCloneSection={handleCloneSection} setIsImportFieldDialogOpen={setIsImportFieldDialogOpen} setImportFieldTargetSectionId={setImportFieldTargetSectionId} /> {/* 계층구조 탭 */} ({ value: opt.value, label: opt.label }))} editingPageId={editingPageId} setEditingPageId={setEditingPageId} editingPageName={editingPageName} setEditingPageName={setEditingPageName} selectedPageId={selectedPageId} setSelectedPageId={setSelectedPageId} editingPathPageId={editingPathPageId} setEditingPathPageId={setEditingPathPageId} editingAbsolutePath={editingAbsolutePath} setEditingAbsolutePath={setEditingAbsolutePath} editingSectionId={editingSectionId} setEditingSectionId={setEditingSectionId} editingSectionTitle={editingSectionTitle} setEditingSectionTitle={setEditingSectionTitle} hasUnsavedChanges={false} pendingChanges={{ pages: [], sections: [], fields: [], masterFields: [], attributes: [], sectionTemplates: [] }} selectedSectionForField={selectedSectionForField} setSelectedSectionForField={setSelectedSectionForField} newSectionType={newSectionType} setNewSectionType={setNewSectionTypeWrapper} updateItemPage={updateItemPage} trackChange={() => {}} deleteItemPage={handleDeletePageWithTracking} duplicatePage={handleDuplicatePage} setIsPageDialogOpen={setIsPageDialogOpen} setIsSectionDialogOpen={setIsSectionDialogOpen} setIsFieldDialogOpen={setIsFieldDialogOpen} handleEditSectionTitle={handleEditSectionTitle} handleSaveSectionTitle={handleSaveSectionTitleWrapper} moveSection={moveSection} unlinkSection={handleUnlinkSection} updateSection={updateSection} deleteField={handleUnlinkFieldWithTracking} handleEditField={handleEditField} moveField={moveField} setIsImportSectionDialogOpen={setIsImportSectionDialogOpen} setIsImportFieldDialogOpen={setIsImportFieldDialogOpen} setImportFieldTargetSectionId={setImportFieldTargetSectionId} // 2025-11-27 추가: BOM 항목 API 함수 addBOMItem={addBOMItem} updateBOMItem={updateBOMItem} deleteBOMItem={deleteBOMItem} /> {/* 사용자 정의 탭들 */} {customTabs.filter(tab => !tab.isDefault).map(tab => ( {tab.label} 사용자 정의 탭입니다. 여기에 필요한 콘텐츠를 추가할 수 있습니다.

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

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

))}
{}} /> {/* 항목 추가/수정 다이얼로그 - 데스크톱 */} {!isMobile && ( s.id === selectedSectionForField) || null} selectedPage={selectedPage || null} itemMasterFields={itemMasterFields} handleAddField={handleAddFieldWrapper} setIsColumnDialogOpen={setIsColumnDialogOpen} setEditingColumnId={setEditingColumnId} setColumnName={setColumnName} setColumnKey={setColumnKey} /> )} {/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */} {isMobile && ( s.id === selectedSectionForField) || null} selectedPage={selectedPage || null} itemMasterFields={itemMasterFields} handleAddField={handleAddFieldWrapper} setIsColumnDialogOpen={setIsColumnDialogOpen} setEditingColumnId={setEditingColumnId} setColumnName={setColumnName} setColumnKey={setColumnKey} /> )} {/* 텍스트박스 컬럼 추가/수정 다이얼로그 */} {/* 섹션 불러오기 다이얼로그 */} {/* 필드 불러오기 다이얼로그 - 2025-11-27: 탭 통합 (항목+독립필드 → 필드) */} s.id === importFieldTargetSectionId)?.title : undefined } />
); }