refactor: 품목기준관리 설정 페이지 훅/컴포넌트 분리
- Phase 1: 신규 훅 4개 생성 - useInitialDataLoading.ts (초기 데이터 로딩) - useImportManagement.ts (섹션/필드 Import) - useReorderManagement.ts (드래그앤드롭 순서 변경) - useDeleteManagement.ts (삭제/언링크 핸들러) - Phase 2: UI 컴포넌트 2개 생성 - AttributeTabContent.tsx (속성 탭 콘텐츠) - ItemMasterDialogs.tsx (다이얼로그 통합) - 메인 컴포넌트 1,799줄 → ~1,478줄 (약 320줄 감소) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,42 +6,13 @@ 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';
|
||||
// 2025-12-24: Phase 2 UI 컴포넌트 분리
|
||||
import { AttributeTabContent, ItemMasterDialogs } from './ItemMasterDataManagement/components';
|
||||
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';
|
||||
@@ -58,6 +29,11 @@ import {
|
||||
useTemplateManagement,
|
||||
useAttributeManagement,
|
||||
useTabManagement,
|
||||
// 2025-12-24: 신규 훅 추가
|
||||
useInitialDataLoading,
|
||||
useImportManagement,
|
||||
useReorderManagement,
|
||||
useDeleteManagement,
|
||||
} from './ItemMasterDataManagement/hooks';
|
||||
|
||||
const ITEM_TYPE_OPTIONS = [
|
||||
@@ -133,6 +109,87 @@ export function ItemMasterDataManagement() {
|
||||
const attributeManagement = useAttributeManagement();
|
||||
const tabManagement = useTabManagement();
|
||||
|
||||
// 2025-12-24: 신규 훅 (먼저 tabManagement, attributeManagement에서 setter 추출 필요)
|
||||
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 {
|
||||
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;
|
||||
|
||||
// 2025-12-24: 신규 훅 초기화
|
||||
const { isInitialLoading, error } = useInitialDataLoading({
|
||||
setCustomTabs,
|
||||
setUnitOptions,
|
||||
});
|
||||
|
||||
const importManagement = useImportManagement();
|
||||
const {
|
||||
isImportSectionDialogOpen, setIsImportSectionDialogOpen,
|
||||
isImportFieldDialogOpen, setIsImportFieldDialogOpen,
|
||||
selectedImportSectionId, setSelectedImportSectionId,
|
||||
selectedImportFieldId, setSelectedImportFieldId,
|
||||
importFieldTargetSectionId, setImportFieldTargetSectionId,
|
||||
handleImportSection: handleImportSectionFromHook,
|
||||
handleImportField: handleImportFieldFromHook,
|
||||
handleCloneSection,
|
||||
} = importManagement;
|
||||
|
||||
const reorderManagement = useReorderManagement();
|
||||
const { moveSection: moveSectionFromHook, moveField: moveFieldFromHook } = reorderManagement;
|
||||
|
||||
const deleteManagement = useDeleteManagement({ itemPages });
|
||||
const {
|
||||
handleDeletePage: handleDeletePageWithTracking,
|
||||
handleDeleteSection: _handleDeleteSectionWithTracking,
|
||||
handleUnlinkField: handleUnlinkFieldWithTracking,
|
||||
handleResetAllData: handleResetAllDataFromHook,
|
||||
} = deleteManagement;
|
||||
|
||||
// 훅에서 필요한 값들 구조분해
|
||||
const {
|
||||
selectedPageId, setSelectedPageId, selectedPage,
|
||||
@@ -233,57 +290,6 @@ export function ItemMasterDataManagement() {
|
||||
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 => ({
|
||||
@@ -371,129 +377,6 @@ export function ItemMasterDataManagement() {
|
||||
return uniqueSections;
|
||||
}, [itemPages, independentSections]);
|
||||
|
||||
// 마운트 상태 추적 (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(() => {
|
||||
@@ -508,122 +391,21 @@ export function ItemMasterDataManagement() {
|
||||
// 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-12-02: 섹션별 순서 종속 - 해당 섹션의 마지막 순서 + 1로 설정
|
||||
const targetSection = selectedPage?.sections.find(s => s.id === importFieldTargetSectionId);
|
||||
const existingFieldsCount = targetSection?.fields?.length ?? 0;
|
||||
const newOrderNo = existingFieldsCount; // 0-based로 마지막 다음 순서
|
||||
|
||||
// 2025-11-27: 통합된 필드 연결 방식
|
||||
await linkFieldToSection(importFieldTargetSectionId, selectedImportFieldId, newOrderNo);
|
||||
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('페이지 이름이 업데이트되었습니다');
|
||||
};
|
||||
// 2025-12-24: 신규 훅에서 가져온 핸들러 래퍼
|
||||
const handleImportSection = async () => handleImportSectionFromHook(selectedPageId);
|
||||
const handleImportField = async () => handleImportFieldFromHook(selectedPage);
|
||||
const moveSection = async (dragIndex: number, hoverIndex: number) => moveSectionFromHook(selectedPage, dragIndex, hoverIndex);
|
||||
const moveField = async (sectionId: number, dragFieldId: number, hoverFieldId: number) => moveFieldFromHook(selectedPage, sectionId, dragFieldId, hoverFieldId);
|
||||
const handleResetAllData = () => handleResetAllDataFromHook(
|
||||
setUnitOptions,
|
||||
setMaterialOptions,
|
||||
setSurfaceTreatmentOptions,
|
||||
setCustomAttributeOptions,
|
||||
setAttributeColumns,
|
||||
setBomItems,
|
||||
setCustomTabs,
|
||||
setAttributeSubTabs,
|
||||
);
|
||||
|
||||
// ===== 래퍼 함수들 (훅 함수에 selectedPage 바인딩 및 타입 호환성) =====
|
||||
const handleAddSectionWrapper = () => handleAddSection(selectedPage);
|
||||
@@ -638,110 +420,6 @@ export function ItemMasterDataManagement() {
|
||||
// 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('섹션 순서 변경에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 필드 순서 변경 핸들러
|
||||
// 2025-12-03: ID 기반으로 변경 (index stale 문제 해결)
|
||||
const moveField = async (sectionId: number, dragFieldId: number, hoverFieldId: number) => {
|
||||
if (!selectedPage) return;
|
||||
const section = selectedPage.sections.find(s => s.id === sectionId);
|
||||
if (!section || !section.fields) return;
|
||||
|
||||
// 동일 필드면 스킵
|
||||
if (dragFieldId === hoverFieldId) return;
|
||||
|
||||
// 정렬된 배열에서 ID로 인덱스 찾기
|
||||
const sortedFields = [...section.fields].sort((a, b) => (a.order_no ?? 0) - (b.order_no ?? 0));
|
||||
const dragIndex = sortedFields.findIndex(f => f.id === dragFieldId);
|
||||
const hoverIndex = sortedFields.findIndex(f => f.id === hoverFieldId);
|
||||
|
||||
// 유효하지 않은 인덱스 체크
|
||||
if (dragIndex === -1 || hoverIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 드래그된 필드를 제거하고 새 위치에 삽입
|
||||
const [draggedField] = sortedFields.splice(dragIndex, 1);
|
||||
sortedFields.splice(hoverIndex, 0, draggedField);
|
||||
|
||||
const newFieldIds = sortedFields.map(f => f.id);
|
||||
|
||||
try {
|
||||
await reorderFields(sectionId, newFieldIds);
|
||||
toast.success('항목 순서가 변경되었습니다');
|
||||
} catch (error) {
|
||||
toast.error('항목 순서 변경에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 전체 데이터 초기화 핸들러
|
||||
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 (
|
||||
|
||||
Reference in New Issue
Block a user