- 페이지 삭제 시 독립 섹션 목록 갱신 추가 (독립 엔티티 아키텍처) - ItemForm 컴포넌트 분리 완료 (1607→415줄, 74% 감소) - ItemMasterDataManagement 중복 코드 제거 (getInputTypeLabel 헬퍼) - 문서 업데이트 (realtime-sync-fixes.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1798 lines
84 KiB
TypeScript
1798 lines
84 KiB
TypeScript
'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<string, string> = {
|
|
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<string | null>(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<BOMItem[]>([]);
|
|
|
|
// 2025-11-26 추가: 섹션/필드 불러오기 다이얼로그 상태
|
|
const [isImportSectionDialogOpen, setIsImportSectionDialogOpen] = useState(false);
|
|
const [isImportFieldDialogOpen, setIsImportFieldDialogOpen] = useState(false);
|
|
const [selectedImportSectionId, setSelectedImportSectionId] = useState<number | null>(null);
|
|
const [selectedImportFieldId, setSelectedImportFieldId] = useState<number | null>(null);
|
|
const [importFieldTargetSectionId, setImportFieldTargetSectionId] = useState<number | null>(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<SetStateAction> 타입 호환성)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const setNewSectionTypeWrapper: React.Dispatch<React.SetStateAction<'fields' | 'bom'>> = setNewSectionType as any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const setNewPageItemTypeWrapper: React.Dispatch<React.SetStateAction<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>> = 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 (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<LoadingSpinner size="lg" text="데이터를 불러오는 중..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 에러 발생 시 UI
|
|
if (error) {
|
|
return (
|
|
<ServerErrorPage
|
|
title="데이터를 불러올 수 없습니다"
|
|
message={error}
|
|
onRetry={() => window.location.reload()}
|
|
showContactInfo={true}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageLayout>
|
|
<PageHeader
|
|
title="품목기준관리"
|
|
description="품목관리에서 사용되는 기준 정보를 설정하고 관리합니다"
|
|
icon={Database}
|
|
/>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<TabsList className="flex-1">
|
|
{customTabs.sort((a, b) => a.order - b.order).map(tab => {
|
|
const Icon = getTabIcon(tab.icon);
|
|
return (
|
|
<TabsTrigger key={tab.id} value={tab.id}>
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
{tab.label}
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
{/*<Button*/}
|
|
{/* size="sm"*/}
|
|
{/* variant="outline"*/}
|
|
{/* onClick={() => setIsManageTabsDialogOpen(true)}*/}
|
|
{/*>*/}
|
|
{/* <Settings className="h-4 w-4 mr-1" />*/}
|
|
{/* 탭 관리*/}
|
|
{/*</Button>*/}
|
|
{/* 전체 초기화 버튼 숨김 처리 - 디자인에 없는 기능 */}
|
|
{/* <Button
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={handleResetAllData}
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
전체 초기화
|
|
</Button> */}
|
|
</div>
|
|
|
|
{/* 속성 탭 (단위/재질/표면처리 통합) */}
|
|
<TabsContent value="attributes" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>속성 관리</CardTitle>
|
|
<CardDescription>단위, 재질, 표면처리 등의 속성을 관리합니다</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* 속성 하위 탭 (칩 형태) */}
|
|
<div className="flex items-center gap-2 mb-6 border-b pb-2">
|
|
<div className="flex gap-2 flex-1 flex-wrap">
|
|
{attributeSubTabs.sort((a, b) => a.order - b.order).map(tab => (
|
|
<Button
|
|
key={tab.id}
|
|
variant={activeAttributeTab === tab.key ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setActiveAttributeTab(tab.key)}
|
|
className="rounded-full"
|
|
>
|
|
{tab.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setIsManageAttributeTabsDialogOpen(true)}
|
|
className="shrink-0"
|
|
>
|
|
<Settings className="w-4 h-4 mr-1" />
|
|
항목 관리
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 단위 관리 */}
|
|
{activeAttributeTab === 'units' && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-medium">단위 목록</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => {
|
|
setManagingColumnType('units');
|
|
setNewColumnName('');
|
|
setNewColumnKey('');
|
|
setNewColumnType('text');
|
|
setNewColumnRequired(false);
|
|
setIsColumnManageDialogOpen(true);
|
|
}}>
|
|
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
|
</Button>
|
|
<Button size="sm" onClick={() => { setEditingOptionType('unit'); setIsOptionDialogOpen(true); }}>
|
|
<Plus className="w-4 h-4 mr-2" />추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{unitOptions.map((option) => {
|
|
const columns = attributeColumns['units'] || [];
|
|
const hasColumns = columns.length > 0 && option.columnValues;
|
|
|
|
return (
|
|
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-base">{option.label}</span>
|
|
{option.inputType && (
|
|
<Badge variant="outline" className="text-xs">{getInputTypeLabel(option.inputType)}</Badge>
|
|
)}
|
|
{option.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">값(Value):</span>
|
|
<span>{option.value}</span>
|
|
</div>
|
|
{option.placeholder && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">플레이스홀더:</span>
|
|
<span>{option.placeholder}</span>
|
|
</div>
|
|
)}
|
|
{option.defaultValue && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">기본값:</span>
|
|
<span>{option.defaultValue}</span>
|
|
</div>
|
|
)}
|
|
{option.inputType === 'dropdown' && option.options && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">옵션:</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{option.options.map((opt, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasColumns && (
|
|
<div className="mt-3 pt-3 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{columns.map((column) => (
|
|
<div key={column.id} className="flex gap-2">
|
|
<span className="text-muted-foreground">{column.name}:</span>
|
|
<span>{option.columnValues?.[column.key] || '-'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption('unit', option.id)}>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 재질 관리 */}
|
|
{activeAttributeTab === 'materials' && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-medium">재질 목록</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => {
|
|
setManagingColumnType('materials');
|
|
setNewColumnName('');
|
|
setNewColumnKey('');
|
|
setNewColumnType('text');
|
|
setNewColumnRequired(false);
|
|
setIsColumnManageDialogOpen(true);
|
|
}}>
|
|
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
|
</Button>
|
|
<Button size="sm" onClick={() => { setEditingOptionType('material'); setIsOptionDialogOpen(true); }}>
|
|
<Plus className="w-4 h-4 mr-2" />추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{materialOptions.map((option) => {
|
|
const columns = attributeColumns['materials'] || [];
|
|
const hasColumns = columns.length > 0 && option.columnValues;
|
|
|
|
return (
|
|
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-base">{option.label}</span>
|
|
{option.inputType && (
|
|
<Badge variant="outline" className="text-xs">{getInputTypeLabel(option.inputType)}</Badge>
|
|
)}
|
|
{option.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">값(Value):</span>
|
|
<span>{option.value}</span>
|
|
</div>
|
|
{option.placeholder && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">플레이스홀더:</span>
|
|
<span>{option.placeholder}</span>
|
|
</div>
|
|
)}
|
|
{option.defaultValue && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">기본값:</span>
|
|
<span>{option.defaultValue}</span>
|
|
</div>
|
|
)}
|
|
{option.inputType === 'dropdown' && option.options && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">옵션:</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{option.options.map((opt, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasColumns && (
|
|
<div className="mt-3 pt-3 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{columns.map((column) => (
|
|
<div key={column.id} className="flex gap-2">
|
|
<span className="text-muted-foreground">{column.name}:</span>
|
|
<span>{option.columnValues?.[column.key] || '-'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption('material', option.id)}>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 표면처리 관리 */}
|
|
{activeAttributeTab === 'surface' && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-medium">표면처리 목록</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => {
|
|
setManagingColumnType('surface');
|
|
setNewColumnName('');
|
|
setNewColumnKey('');
|
|
setNewColumnType('text');
|
|
setNewColumnRequired(false);
|
|
setIsColumnManageDialogOpen(true);
|
|
}}>
|
|
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
|
</Button>
|
|
<Button size="sm" onClick={() => { setEditingOptionType('surface'); setIsOptionDialogOpen(true); }}>
|
|
<Plus className="w-4 h-4 mr-2" />추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{surfaceTreatmentOptions.map((option) => {
|
|
const columns = attributeColumns['surface'] || [];
|
|
const hasColumns = columns.length > 0 && option.columnValues;
|
|
const inputTypeLabel = getInputTypeLabel(option.inputType);
|
|
|
|
return (
|
|
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-base">{option.label}</span>
|
|
{option.inputType && (
|
|
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
|
|
)}
|
|
{option.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">값(Value):</span>
|
|
<span>{option.value}</span>
|
|
</div>
|
|
{option.placeholder && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">플레이스홀더:</span>
|
|
<span>{option.placeholder}</span>
|
|
</div>
|
|
)}
|
|
{option.defaultValue && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">기본값:</span>
|
|
<span>{option.defaultValue}</span>
|
|
</div>
|
|
)}
|
|
{option.inputType === 'dropdown' && option.options && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">옵션:</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{option.options.map((opt, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasColumns && (
|
|
<div className="mt-3 pt-3 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{columns.map((column) => (
|
|
<div key={column.id} className="flex gap-2">
|
|
<span className="text-muted-foreground">{column.name}:</span>
|
|
<span>{option.columnValues?.[column.key] || '-'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption('surface', option.id)}>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 사용자 정의 속성 탭 및 마스터 항목 탭 */}
|
|
{!['units', 'materials', 'surface'].includes(activeAttributeTab) && (() => {
|
|
const currentTabKey = activeAttributeTab;
|
|
|
|
// 마스터 항목인지 확인
|
|
const masterField = itemMasterFields.find(f => f.id.toString() === currentTabKey);
|
|
|
|
// 마스터 항목이면 해당 항목의 속성값들을 표시
|
|
// Note: properties is Record<string, any> | 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 (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="font-medium">{masterField.field_name} 속성 목록</h3>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
항목 탭에서 추가한 "{masterField.field_name}" 항목의 속성값들입니다
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
{propertiesArray.map((property: any) => {
|
|
const inputTypeLabel = getInputTypeLabel(property.type);
|
|
|
|
return (
|
|
<div key={property.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-base">{property.label}</span>
|
|
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
|
|
{property.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-24">키(Key):</span>
|
|
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{property.key}</code>
|
|
</div>
|
|
{property.placeholder && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-24">플레이스홀더:</span>
|
|
<span>{property.placeholder}</span>
|
|
</div>
|
|
)}
|
|
{property.defaultValue && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-24">기본값:</span>
|
|
<span>{property.defaultValue}</span>
|
|
</div>
|
|
)}
|
|
{property.type === 'dropdown' && property.options && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-24">옵션:</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{property.options.map((opt: string, idx: number) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex items-start gap-2">
|
|
<Package className="w-5 h-5 text-blue-600 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-blue-900">
|
|
마스터 항목 속성 관리
|
|
</p>
|
|
<p className="text-xs text-blue-700 mt-1">
|
|
이 속성들은 <strong>항목 탭</strong>에서 "{masterField.field_name}" 항목을 편집하여 추가/수정/삭제할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 사용자 정의 속성 탭 (기존 로직)
|
|
const currentOptions = customAttributeOptions[currentTabKey] || [];
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="font-medium">
|
|
{attributeSubTabs.find(t => t.key === activeAttributeTab)?.label || '사용자 정의'} 목록
|
|
</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={() => {
|
|
setManagingColumnType(currentTabKey);
|
|
setNewColumnName('');
|
|
setNewColumnKey('');
|
|
setNewColumnType('text');
|
|
setNewColumnRequired(false);
|
|
setIsColumnManageDialogOpen(true);
|
|
}}>
|
|
<Settings className="w-4 h-4 mr-2" />칼럼 관리
|
|
</Button>
|
|
<Button size="sm" onClick={() => {
|
|
setEditingOptionType(activeAttributeTab);
|
|
setNewOptionValue('');
|
|
setNewOptionLabel('');
|
|
setNewOptionColumnValues({});
|
|
setIsOptionDialogOpen(true);
|
|
}}>
|
|
<Plus className="w-4 h-4 mr-2" />추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{currentOptions.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{currentOptions.map((option) => {
|
|
const columns = attributeColumns[currentTabKey] || [];
|
|
const hasColumns = columns.length > 0 && option.columnValues;
|
|
const inputTypeLabel = getInputTypeLabel(option.inputType);
|
|
|
|
return (
|
|
<div key={option.id} className="p-4 border rounded hover:bg-gray-50 transition-colors">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-base">{option.label}</span>
|
|
<Badge variant="outline" className="text-xs">{inputTypeLabel}</Badge>
|
|
{option.required && (
|
|
<Badge variant="destructive" className="text-xs">필수</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">값(Value):</span>
|
|
<span>{option.value}</span>
|
|
</div>
|
|
{option.placeholder && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">플레이스홀더:</span>
|
|
<span>{option.placeholder}</span>
|
|
</div>
|
|
)}
|
|
{option.defaultValue && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">기본값:</span>
|
|
<span>{option.defaultValue}</span>
|
|
</div>
|
|
)}
|
|
{option.inputType === 'dropdown' && option.options && (
|
|
<div className="flex gap-2">
|
|
<span className="font-medium min-w-16">옵션:</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{option.options.map((opt, idx) => (
|
|
<Badge key={idx} variant="secondary" className="text-xs">{opt}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasColumns && (
|
|
<div className="mt-3 pt-3 border-t">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2">추가 칼럼</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
{columns.map((column) => (
|
|
<div key={column.id} className="flex gap-2">
|
|
<span className="text-muted-foreground">{column.name}:</span>
|
|
<span>{option.columnValues?.[column.key] || '-'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={() => handleDeleteOption(currentTabKey, option.id)}>
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="border-2 border-dashed rounded-lg p-8 text-center text-gray-500">
|
|
<Settings className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
|
<p className="mb-2">아직 추가된 항목이 없습니다</p>
|
|
<p className="text-sm">위 "추가" 버튼을 클릭하여 새로운 속성을 추가할 수 있습니다</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})()}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 항목 탭 */}
|
|
<TabsContent value="items" className="space-y-4">
|
|
<MasterFieldTab
|
|
itemMasterFields={itemMasterFields}
|
|
setIsMasterFieldDialogOpen={setIsMasterFieldDialogOpen}
|
|
handleEditMasterField={handleEditMasterField}
|
|
handleDeleteMasterField={handleDeleteMasterField}
|
|
hasUnsavedChanges={false}
|
|
pendingChanges={{ masterFields: [] }}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* 섹션관리 탭 */}
|
|
<TabsContent value="sections" className="space-y-4">
|
|
<SectionsTab
|
|
sectionTemplates={sectionsAsTemplates}
|
|
setIsSectionTemplateDialogOpen={setIsSectionTemplateDialogOpen}
|
|
setCurrentTemplateId={setCurrentTemplateId}
|
|
setIsTemplateFieldDialogOpen={setIsTemplateFieldDialogOpen}
|
|
handleEditSectionTemplate={handleEditSectionTemplate}
|
|
handleDeleteSectionTemplate={handleDeleteSectionTemplate}
|
|
handleEditTemplateField={handleEditTemplateField}
|
|
handleDeleteTemplateField={handleDeleteTemplateField}
|
|
handleAddBOMItemToTemplate={handleAddBOMItemToTemplate}
|
|
handleUpdateBOMItemInTemplate={handleUpdateBOMItemInTemplate}
|
|
handleDeleteBOMItemFromTemplate={handleDeleteBOMItemFromTemplate}
|
|
ITEM_TYPE_OPTIONS={ITEM_TYPE_OPTIONS}
|
|
INPUT_TYPE_OPTIONS={INPUT_TYPE_OPTIONS}
|
|
unitOptions={unitOptions.map(opt => ({ value: opt.value, label: opt.label }))}
|
|
onCloneSection={handleCloneSection}
|
|
setIsImportFieldDialogOpen={setIsImportFieldDialogOpen}
|
|
setImportFieldTargetSectionId={setImportFieldTargetSectionId}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* 계층구조 탭 */}
|
|
<TabsContent value="hierarchy" className="space-y-4">
|
|
<HierarchyTab
|
|
itemPages={itemPages}
|
|
selectedPage={selectedPage}
|
|
ITEM_TYPE_OPTIONS={ITEM_TYPE_OPTIONS}
|
|
unitOptions={unitOptions.map(opt => ({ 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}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* 사용자 정의 탭들 */}
|
|
{customTabs.filter(tab => !tab.isDefault).map(tab => (
|
|
<TabsContent key={tab.id} value={tab.id}>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{tab.label}</CardTitle>
|
|
<CardDescription>사용자 정의 탭입니다. 여기에 필요한 콘텐츠를 추가할 수 있습니다.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-center py-12">
|
|
<FileText className="w-16 h-16 mx-auto text-gray-300 mb-4" />
|
|
<p className="text-muted-foreground mb-2">{tab.label} 탭의 콘텐츠가 비어있습니다</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
이 탭에 필요한 기능을 추가하여 사용하세요
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
|
|
<TabManagementDialogs
|
|
isManageTabsDialogOpen={isManageTabsDialogOpen}
|
|
setIsManageTabsDialogOpen={setIsManageTabsDialogOpen}
|
|
customTabs={customTabs}
|
|
moveTabUp={moveTabUp}
|
|
moveTabDown={moveTabDown}
|
|
handleEditTabFromManage={handleEditTabFromManage}
|
|
handleDeleteTab={handleDeleteTab}
|
|
getTabIcon={getTabIcon}
|
|
setIsAddTabDialogOpen={setIsAddTabDialogOpen}
|
|
isDeleteTabDialogOpen={isDeleteTabDialogOpen}
|
|
setIsDeleteTabDialogOpen={setIsDeleteTabDialogOpen}
|
|
deletingTabId={deletingTabId}
|
|
setDeletingTabId={setDeletingTabId}
|
|
confirmDeleteTab={confirmDeleteTab}
|
|
isAddTabDialogOpen={isAddTabDialogOpen}
|
|
editingTabId={editingTabId}
|
|
setEditingTabId={setEditingTabId}
|
|
newTabLabel={newTabLabel}
|
|
setNewTabLabel={setNewTabLabel}
|
|
handleUpdateTab={handleUpdateTab}
|
|
handleAddTab={handleAddTab}
|
|
isManageAttributeTabsDialogOpen={isManageAttributeTabsDialogOpen}
|
|
setIsManageAttributeTabsDialogOpen={setIsManageAttributeTabsDialogOpen}
|
|
attributeSubTabs={attributeSubTabs}
|
|
moveAttributeTabUp={moveAttributeTabUp}
|
|
moveAttributeTabDown={moveAttributeTabDown}
|
|
handleDeleteAttributeTab={handleDeleteAttributeTab}
|
|
isDeleteAttributeTabDialogOpen={isDeleteAttributeTabDialogOpen}
|
|
setIsDeleteAttributeTabDialogOpen={setIsDeleteAttributeTabDialogOpen}
|
|
deletingAttributeTabId={deletingAttributeTabId}
|
|
setDeletingAttributeTabId={setDeletingAttributeTabId}
|
|
confirmDeleteAttributeTab={confirmDeleteAttributeTab}
|
|
isAddAttributeTabDialogOpen={isAddAttributeTabDialogOpen}
|
|
setIsAddAttributeTabDialogOpen={setIsAddAttributeTabDialogOpen}
|
|
editingAttributeTabId={editingAttributeTabId}
|
|
setEditingAttributeTabId={setEditingAttributeTabId}
|
|
newAttributeTabLabel={newAttributeTabLabel}
|
|
setNewAttributeTabLabel={setNewAttributeTabLabel}
|
|
handleUpdateAttributeTab={handleUpdateAttributeTab}
|
|
handleAddAttributeTab={handleAddAttributeTab}
|
|
/>
|
|
|
|
<OptionDialog
|
|
isOpen={isOptionDialogOpen}
|
|
setIsOpen={setIsOptionDialogOpen}
|
|
newOptionValue={newOptionValue}
|
|
setNewOptionValue={setNewOptionValue}
|
|
newOptionLabel={newOptionLabel}
|
|
setNewOptionLabel={setNewOptionLabel}
|
|
newOptionColumnValues={newOptionColumnValues}
|
|
setNewOptionColumnValues={setNewOptionColumnValues}
|
|
newOptionInputType={newOptionInputType}
|
|
setNewOptionInputType={setNewOptionInputType}
|
|
newOptionRequired={newOptionRequired}
|
|
setNewOptionRequired={setNewOptionRequired}
|
|
newOptionOptions={newOptionOptions}
|
|
setNewOptionOptions={setNewOptionOptions}
|
|
newOptionPlaceholder={newOptionPlaceholder}
|
|
setNewOptionPlaceholder={setNewOptionPlaceholder}
|
|
newOptionDefaultValue={newOptionDefaultValue}
|
|
setNewOptionDefaultValue={setNewOptionDefaultValue}
|
|
editingOptionType={editingOptionType}
|
|
attributeSubTabs={attributeSubTabs}
|
|
attributeColumns={attributeColumns}
|
|
handleAddOption={handleAddOption}
|
|
/>
|
|
|
|
<ColumnManageDialog
|
|
isOpen={isColumnManageDialogOpen}
|
|
setIsOpen={setIsColumnManageDialogOpen}
|
|
managingColumnType={managingColumnType}
|
|
attributeSubTabs={attributeSubTabs}
|
|
attributeColumns={attributeColumns}
|
|
setAttributeColumns={setAttributeColumns}
|
|
newColumnName={newColumnName}
|
|
setNewColumnName={setNewColumnName}
|
|
newColumnKey={newColumnKey}
|
|
setNewColumnKey={setNewColumnKey}
|
|
newColumnType={newColumnType}
|
|
setNewColumnType={setNewColumnType}
|
|
newColumnRequired={newColumnRequired}
|
|
setNewColumnRequired={setNewColumnRequired}
|
|
/>
|
|
|
|
|
|
<PathEditDialog
|
|
editingPathPageId={editingPathPageId}
|
|
setEditingPathPageId={setEditingPathPageId}
|
|
editingAbsolutePath={editingAbsolutePath}
|
|
setEditingAbsolutePath={setEditingAbsolutePath}
|
|
updateItemPage={updateItemPage}
|
|
trackChange={() => {}}
|
|
/>
|
|
|
|
<PageDialog
|
|
isPageDialogOpen={isPageDialogOpen}
|
|
setIsPageDialogOpen={setIsPageDialogOpen}
|
|
newPageName={newPageName}
|
|
setNewPageName={setNewPageName}
|
|
newPageItemType={newPageItemType}
|
|
setNewPageItemType={setNewPageItemTypeWrapper}
|
|
handleAddPage={handleAddPage}
|
|
/>
|
|
|
|
<SectionDialog
|
|
isSectionDialogOpen={isSectionDialogOpen}
|
|
setIsSectionDialogOpen={setIsSectionDialogOpen}
|
|
newSectionType={newSectionType}
|
|
setNewSectionType={setNewSectionType}
|
|
newSectionTitle={newSectionTitle}
|
|
setNewSectionTitle={setNewSectionTitle}
|
|
newSectionDescription={newSectionDescription}
|
|
setNewSectionDescription={setNewSectionDescription}
|
|
handleAddSection={handleAddSectionWrapper}
|
|
sectionInputMode={sectionInputMode}
|
|
setSectionInputMode={setSectionInputMode}
|
|
sectionTemplates={sectionsAsTemplates}
|
|
selectedTemplateId={selectedSectionTemplateId}
|
|
setSelectedTemplateId={setSelectedSectionTemplateId}
|
|
handleLinkTemplate={handleLinkTemplateWrapper}
|
|
/>
|
|
|
|
{/* 항목 추가/수정 다이얼로그 - 데스크톱 */}
|
|
{!isMobile && (
|
|
<FieldDialog
|
|
isOpen={isFieldDialogOpen}
|
|
onOpenChange={setIsFieldDialogOpen}
|
|
editingFieldId={editingFieldId}
|
|
setEditingFieldId={setEditingFieldId}
|
|
fieldInputMode={fieldInputMode}
|
|
setFieldInputMode={setFieldInputMode}
|
|
showMasterFieldList={showMasterFieldList}
|
|
setShowMasterFieldList={setShowMasterFieldList}
|
|
selectedMasterFieldId={selectedMasterFieldId}
|
|
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
|
textboxColumns={textboxColumns}
|
|
setTextboxColumns={setTextboxColumns}
|
|
newFieldConditionEnabled={newFieldConditionEnabled}
|
|
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
|
newFieldConditionTargetType={newFieldConditionTargetType}
|
|
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
|
newFieldConditionFields={newFieldConditionFields}
|
|
setNewFieldConditionFields={setNewFieldConditionFields}
|
|
newFieldConditionSections={newFieldConditionSections}
|
|
setNewFieldConditionSections={setNewFieldConditionSections}
|
|
tempConditionValue={tempConditionValue}
|
|
setTempConditionValue={setTempConditionValue}
|
|
newFieldName={newFieldName}
|
|
setNewFieldName={setNewFieldName}
|
|
newFieldKey={newFieldKey}
|
|
setNewFieldKey={setNewFieldKey}
|
|
newFieldInputType={newFieldInputType}
|
|
setNewFieldInputType={setNewFieldInputType}
|
|
newFieldRequired={newFieldRequired}
|
|
setNewFieldRequired={setNewFieldRequired}
|
|
newFieldDescription={newFieldDescription}
|
|
setNewFieldDescription={setNewFieldDescription}
|
|
newFieldOptions={newFieldOptions}
|
|
setNewFieldOptions={setNewFieldOptions}
|
|
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
|
selectedPage={selectedPage || null}
|
|
itemMasterFields={itemMasterFields}
|
|
handleAddField={handleAddFieldWrapper}
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
setEditingColumnId={setEditingColumnId}
|
|
setColumnName={setColumnName}
|
|
setColumnKey={setColumnKey}
|
|
/>
|
|
)}
|
|
|
|
{/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */}
|
|
{isMobile && (
|
|
<FieldDrawer
|
|
isOpen={isFieldDialogOpen}
|
|
onOpenChange={setIsFieldDialogOpen}
|
|
editingFieldId={editingFieldId}
|
|
setEditingFieldId={setEditingFieldId}
|
|
fieldInputMode={fieldInputMode}
|
|
setFieldInputMode={setFieldInputMode}
|
|
showMasterFieldList={showMasterFieldList}
|
|
setShowMasterFieldList={setShowMasterFieldList}
|
|
selectedMasterFieldId={selectedMasterFieldId}
|
|
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
|
textboxColumns={textboxColumns}
|
|
setTextboxColumns={setTextboxColumns}
|
|
newFieldConditionEnabled={newFieldConditionEnabled}
|
|
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
|
newFieldConditionTargetType={newFieldConditionTargetType}
|
|
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
|
newFieldConditionFields={newFieldConditionFields}
|
|
setNewFieldConditionFields={setNewFieldConditionFields}
|
|
newFieldConditionSections={newFieldConditionSections}
|
|
setNewFieldConditionSections={setNewFieldConditionSections}
|
|
tempConditionValue={tempConditionValue}
|
|
setTempConditionValue={setTempConditionValue}
|
|
newFieldName={newFieldName}
|
|
setNewFieldName={setNewFieldName}
|
|
newFieldKey={newFieldKey}
|
|
setNewFieldKey={setNewFieldKey}
|
|
newFieldInputType={newFieldInputType}
|
|
setNewFieldInputType={setNewFieldInputType}
|
|
newFieldRequired={newFieldRequired}
|
|
setNewFieldRequired={setNewFieldRequired}
|
|
newFieldDescription={newFieldDescription}
|
|
setNewFieldDescription={setNewFieldDescription}
|
|
newFieldOptions={newFieldOptions}
|
|
setNewFieldOptions={setNewFieldOptions}
|
|
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
|
selectedPage={selectedPage || null}
|
|
itemMasterFields={itemMasterFields}
|
|
handleAddField={handleAddFieldWrapper}
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
setEditingColumnId={setEditingColumnId}
|
|
setColumnName={setColumnName}
|
|
setColumnKey={setColumnKey}
|
|
/>
|
|
)}
|
|
|
|
{/* 텍스트박스 컬럼 추가/수정 다이얼로그 */}
|
|
<ColumnDialog
|
|
isColumnDialogOpen={isColumnDialogOpen}
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
editingColumnId={editingColumnId}
|
|
setEditingColumnId={setEditingColumnId}
|
|
columnName={columnName}
|
|
setColumnName={setColumnName}
|
|
columnKey={columnKey}
|
|
setColumnKey={setColumnKey}
|
|
textboxColumns={textboxColumns}
|
|
setTextboxColumns={setTextboxColumns}
|
|
/>
|
|
|
|
|
|
<MasterFieldDialog
|
|
isMasterFieldDialogOpen={isMasterFieldDialogOpen}
|
|
setIsMasterFieldDialogOpen={setIsMasterFieldDialogOpen}
|
|
editingMasterFieldId={editingMasterFieldId}
|
|
setEditingMasterFieldId={setEditingMasterFieldId}
|
|
newMasterFieldName={newMasterFieldName}
|
|
setNewMasterFieldName={setNewMasterFieldName}
|
|
newMasterFieldKey={newMasterFieldKey}
|
|
setNewMasterFieldKey={setNewMasterFieldKey}
|
|
newMasterFieldInputType={newMasterFieldInputType}
|
|
setNewMasterFieldInputType={setNewMasterFieldInputType}
|
|
newMasterFieldRequired={newMasterFieldRequired}
|
|
setNewMasterFieldRequired={setNewMasterFieldRequired}
|
|
newMasterFieldCategory={newMasterFieldCategory}
|
|
setNewMasterFieldCategory={setNewMasterFieldCategory}
|
|
newMasterFieldDescription={newMasterFieldDescription}
|
|
setNewMasterFieldDescription={setNewMasterFieldDescription}
|
|
newMasterFieldOptions={newMasterFieldOptions}
|
|
setNewMasterFieldOptions={setNewMasterFieldOptions}
|
|
newMasterFieldAttributeType={newMasterFieldAttributeType}
|
|
setNewMasterFieldAttributeType={setNewMasterFieldAttributeType}
|
|
newMasterFieldMultiColumn={newMasterFieldMultiColumn}
|
|
setNewMasterFieldMultiColumn={setNewMasterFieldMultiColumn}
|
|
newMasterFieldColumnCount={newMasterFieldColumnCount}
|
|
setNewMasterFieldColumnCount={setNewMasterFieldColumnCount}
|
|
newMasterFieldColumnNames={newMasterFieldColumnNames}
|
|
setNewMasterFieldColumnNames={setNewMasterFieldColumnNames}
|
|
handleUpdateMasterField={handleUpdateMasterField}
|
|
handleAddMasterField={handleAddMasterField}
|
|
/>
|
|
|
|
|
|
<SectionTemplateDialog
|
|
isSectionTemplateDialogOpen={isSectionTemplateDialogOpen}
|
|
setIsSectionTemplateDialogOpen={setIsSectionTemplateDialogOpen}
|
|
editingSectionTemplateId={editingSectionTemplateId}
|
|
setEditingSectionTemplateId={setEditingSectionTemplateId}
|
|
newSectionTemplateTitle={newSectionTemplateTitle}
|
|
setNewSectionTemplateTitle={setNewSectionTemplateTitle}
|
|
newSectionTemplateDescription={newSectionTemplateDescription}
|
|
setNewSectionTemplateDescription={setNewSectionTemplateDescription}
|
|
newSectionTemplateCategory={newSectionTemplateCategory}
|
|
setNewSectionTemplateCategory={setNewSectionTemplateCategory}
|
|
newSectionTemplateType={newSectionTemplateType}
|
|
setNewSectionTemplateType={setNewSectionTemplateType}
|
|
handleUpdateSectionTemplate={handleUpdateSectionTemplate}
|
|
handleAddSectionTemplate={handleAddSectionTemplate}
|
|
/>
|
|
|
|
|
|
<TemplateFieldDialog
|
|
isTemplateFieldDialogOpen={isTemplateFieldDialogOpen}
|
|
setIsTemplateFieldDialogOpen={setIsTemplateFieldDialogOpen}
|
|
editingTemplateFieldId={editingTemplateFieldId}
|
|
setEditingTemplateFieldId={setEditingTemplateFieldId}
|
|
templateFieldName={templateFieldName}
|
|
setTemplateFieldName={setTemplateFieldName}
|
|
templateFieldKey={templateFieldKey}
|
|
setTemplateFieldKey={setTemplateFieldKey}
|
|
templateFieldInputType={templateFieldInputType}
|
|
setTemplateFieldInputType={setTemplateFieldInputType}
|
|
templateFieldRequired={templateFieldRequired}
|
|
setTemplateFieldRequired={setTemplateFieldRequired}
|
|
templateFieldOptions={templateFieldOptions}
|
|
setTemplateFieldOptions={setTemplateFieldOptions}
|
|
templateFieldDescription={templateFieldDescription}
|
|
setTemplateFieldDescription={setTemplateFieldDescription}
|
|
templateFieldMultiColumn={templateFieldMultiColumn}
|
|
setTemplateFieldMultiColumn={setTemplateFieldMultiColumn}
|
|
templateFieldColumnCount={templateFieldColumnCount}
|
|
setTemplateFieldColumnCount={setTemplateFieldColumnCount}
|
|
templateFieldColumnNames={templateFieldColumnNames}
|
|
setTemplateFieldColumnNames={setTemplateFieldColumnNames}
|
|
handleAddTemplateField={handleAddTemplateField}
|
|
// 마스터 항목 관련 props
|
|
itemMasterFields={itemMasterFields}
|
|
templateFieldInputMode={templateFieldInputMode}
|
|
setTemplateFieldInputMode={setTemplateFieldInputMode}
|
|
showMasterFieldList={templateFieldShowMasterFieldList}
|
|
setShowMasterFieldList={setTemplateFieldShowMasterFieldList}
|
|
selectedMasterFieldId={templateFieldSelectedMasterFieldId}
|
|
setSelectedMasterFieldId={setTemplateFieldSelectedMasterFieldId}
|
|
/>
|
|
|
|
<LoadTemplateDialog
|
|
isLoadTemplateDialogOpen={isLoadTemplateDialogOpen}
|
|
setIsLoadTemplateDialogOpen={setIsLoadTemplateDialogOpen}
|
|
sectionTemplates={sectionTemplates}
|
|
selectedTemplateId={selectedTemplateId}
|
|
setSelectedTemplateId={setSelectedTemplateId}
|
|
handleLoadTemplate={handleLoadTemplateWrapper}
|
|
/>
|
|
|
|
{/* 섹션 불러오기 다이얼로그 */}
|
|
<ImportSectionDialog
|
|
isOpen={isImportSectionDialogOpen}
|
|
setIsOpen={setIsImportSectionDialogOpen}
|
|
independentSections={independentSections}
|
|
selectedSectionId={selectedImportSectionId}
|
|
setSelectedSectionId={setSelectedImportSectionId}
|
|
onImport={handleImportSection}
|
|
onRefresh={refreshIndependentSections}
|
|
onGetUsage={getSectionUsage}
|
|
/>
|
|
|
|
{/* 필드 불러오기 다이얼로그 - 2025-11-27: 탭 통합 (항목+독립필드 → 필드) */}
|
|
<ImportFieldDialog
|
|
isOpen={isImportFieldDialogOpen}
|
|
setIsOpen={setIsImportFieldDialogOpen}
|
|
fields={itemMasterFields}
|
|
selectedFieldId={selectedImportFieldId}
|
|
setSelectedFieldId={setSelectedImportFieldId}
|
|
onImport={handleImportField}
|
|
onRefresh={refreshIndependentFields}
|
|
onGetUsage={getFieldUsage}
|
|
targetSectionTitle={
|
|
importFieldTargetSectionId
|
|
? selectedPage?.sections.find(s => s.id === importFieldTargetSectionId)?.title
|
|
: undefined
|
|
}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
}
|