- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
472 lines
15 KiB
TypeScript
472 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
|
import {
|
|
FolderTree,
|
|
ListTree,
|
|
FileText,
|
|
Settings,
|
|
Layers,
|
|
Database,
|
|
Plus,
|
|
Folder
|
|
} from 'lucide-react';
|
|
|
|
export interface CustomTab {
|
|
id: string;
|
|
label: string;
|
|
icon: string;
|
|
isDefault: boolean;
|
|
order: number;
|
|
}
|
|
|
|
export interface AttributeSubTab {
|
|
id: string;
|
|
label: string;
|
|
key: string;
|
|
isDefault: boolean;
|
|
order: number;
|
|
}
|
|
|
|
export interface UseTabManagementReturn {
|
|
// 메인 탭 상태
|
|
customTabs: CustomTab[];
|
|
setCustomTabs: React.Dispatch<React.SetStateAction<CustomTab[]>>;
|
|
activeTab: string;
|
|
setActiveTab: (tab: string) => void;
|
|
|
|
// 속성 하위 탭 상태
|
|
attributeSubTabs: AttributeSubTab[];
|
|
setAttributeSubTabs: React.Dispatch<React.SetStateAction<AttributeSubTab[]>>;
|
|
activeAttributeTab: string;
|
|
setActiveAttributeTab: (tab: string) => void;
|
|
|
|
// 메인 탭 다이얼로그 상태
|
|
isAddTabDialogOpen: boolean;
|
|
setIsAddTabDialogOpen: (open: boolean) => void;
|
|
isManageTabsDialogOpen: boolean;
|
|
setIsManageTabsDialogOpen: (open: boolean) => void;
|
|
newTabLabel: string;
|
|
setNewTabLabel: (label: string) => void;
|
|
editingTabId: string | null;
|
|
setEditingTabId: (id: string | null) => void;
|
|
deletingTabId: string | null;
|
|
setDeletingTabId: (id: string | null) => void;
|
|
isDeleteTabDialogOpen: boolean;
|
|
setIsDeleteTabDialogOpen: (open: boolean) => void;
|
|
|
|
// 속성 하위 탭 다이얼로그 상태
|
|
isManageAttributeTabsDialogOpen: boolean;
|
|
setIsManageAttributeTabsDialogOpen: (open: boolean) => void;
|
|
isAddAttributeTabDialogOpen: boolean;
|
|
setIsAddAttributeTabDialogOpen: (open: boolean) => void;
|
|
newAttributeTabLabel: string;
|
|
setNewAttributeTabLabel: (label: string) => void;
|
|
editingAttributeTabId: string | null;
|
|
setEditingAttributeTabId: (id: string | null) => void;
|
|
deletingAttributeTabId: string | null;
|
|
setDeletingAttributeTabId: (id: string | null) => void;
|
|
isDeleteAttributeTabDialogOpen: boolean;
|
|
setIsDeleteAttributeTabDialogOpen: (open: boolean) => void;
|
|
|
|
// 핸들러
|
|
handleAddTab: () => void;
|
|
handleUpdateTab: () => void;
|
|
handleDeleteTab: (tabId: string) => void;
|
|
confirmDeleteTab: () => void;
|
|
handleAddAttributeTab: () => void;
|
|
handleUpdateAttributeTab: () => void;
|
|
handleDeleteAttributeTab: (tabId: string) => void;
|
|
confirmDeleteAttributeTab: () => void;
|
|
moveTabUp: (tabId: string) => void;
|
|
moveTabDown: (tabId: string) => void;
|
|
moveAttributeTabUp: (tabId: string) => void;
|
|
moveAttributeTabDown: (tabId: string) => void;
|
|
getTabIcon: (iconName: string) => any;
|
|
handleEditTabFromManage: (tab: CustomTab) => void;
|
|
}
|
|
|
|
export function useTabManagement(): UseTabManagementReturn {
|
|
const { itemMasterFields } = useItemMaster();
|
|
|
|
// 메인 탭 상태
|
|
const [customTabs, setCustomTabs] = useState<CustomTab[]>([
|
|
{ id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 },
|
|
{ id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 },
|
|
{ id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 },
|
|
{ id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 }
|
|
]);
|
|
const [activeTab, setActiveTab] = useState('hierarchy');
|
|
|
|
// 속성 하위 탭 상태 (기본 탭: 단위, 재질, 표면처리)
|
|
// TODO: 나중에 백엔드에서 기준값 로드로 대체 예정
|
|
const [attributeSubTabs, setAttributeSubTabs] = useState<AttributeSubTab[]>([
|
|
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
|
|
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
|
|
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 },
|
|
]);
|
|
const [activeAttributeTab, setActiveAttributeTab] = useState('units');
|
|
|
|
// 메인 탭 다이얼로그 상태
|
|
const [isAddTabDialogOpen, setIsAddTabDialogOpen] = useState(false);
|
|
const [isManageTabsDialogOpen, setIsManageTabsDialogOpen] = useState(false);
|
|
const [newTabLabel, setNewTabLabel] = useState('');
|
|
const [editingTabId, setEditingTabId] = useState<string | null>(null);
|
|
const [deletingTabId, setDeletingTabId] = useState<string | null>(null);
|
|
const [isDeleteTabDialogOpen, setIsDeleteTabDialogOpen] = useState(false);
|
|
|
|
// 속성 하위 탭 다이얼로그 상태
|
|
const [isManageAttributeTabsDialogOpen, setIsManageAttributeTabsDialogOpen] = useState(false);
|
|
const [isAddAttributeTabDialogOpen, setIsAddAttributeTabDialogOpen] = useState(false);
|
|
const [newAttributeTabLabel, setNewAttributeTabLabel] = useState('');
|
|
const [editingAttributeTabId, setEditingAttributeTabId] = useState<string | null>(null);
|
|
const [deletingAttributeTabId, setDeletingAttributeTabId] = useState<string | null>(null);
|
|
const [isDeleteAttributeTabDialogOpen, setIsDeleteAttributeTabDialogOpen] = useState(false);
|
|
|
|
// 이전 필드 상태 추적용 ref (무한 루프 방지)
|
|
const prevFieldsRef = useRef<string>('');
|
|
|
|
// 마스터 항목이 추가/수정/삭제될 때 속성 탭 자동 동기화
|
|
useEffect(() => {
|
|
// 현재 필드 상태를 문자열로 직렬화
|
|
const currentFieldsState = JSON.stringify(
|
|
itemMasterFields.map(f => ({ id: f.id, name: f.field_name })).sort((a, b) => a.id - b.id)
|
|
);
|
|
|
|
// 이전 상태와 동일하면 업데이트 스킵
|
|
if (prevFieldsRef.current === currentFieldsState) {
|
|
return;
|
|
}
|
|
prevFieldsRef.current = currentFieldsState;
|
|
|
|
// 현재 마스터 필드 ID 목록
|
|
const currentFieldIds = new Set(itemMasterFields.map(f => f.id.toString()));
|
|
|
|
setAttributeSubTabs(prev => {
|
|
const newTabs: AttributeSubTab[] = [];
|
|
const updates: { key: string; label: string }[] = [];
|
|
|
|
// 삭제된 마스터 항목에 해당하는 탭 제거 (숫자 key만 체크 - 마스터 항목 ID)
|
|
const filteredTabs = prev.filter(tab => {
|
|
// 숫자로만 이루어진 key는 마스터 항목 ID
|
|
const isNumericKey = /^\d+$/.test(tab.key);
|
|
if (isNumericKey && !currentFieldIds.has(tab.key)) {
|
|
// 삭제된 마스터 항목의 탭
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// 새로운 마스터 항목 추가 또는 기존 항목 라벨 업데이트
|
|
itemMasterFields.forEach(field => {
|
|
const existingTab = filteredTabs.find(tab => tab.key === field.id.toString());
|
|
|
|
if (!existingTab) {
|
|
const maxOrder = Math.max(...filteredTabs.map(t => t.order), ...newTabs.map(t => t.order), -1);
|
|
newTabs.push({
|
|
id: `attr-${field.id.toString()}`,
|
|
label: field.field_name,
|
|
key: field.id.toString(),
|
|
isDefault: false,
|
|
order: maxOrder + 1
|
|
});
|
|
} else if (existingTab.label !== field.field_name) {
|
|
updates.push({ key: existingTab.key, label: field.field_name });
|
|
}
|
|
});
|
|
|
|
// 탭 삭제, 추가, 업데이트 여부 확인
|
|
const hasRemovals = filteredTabs.length !== prev.length;
|
|
const hasAdditions = newTabs.length > 0;
|
|
const hasUpdates = updates.length > 0;
|
|
|
|
// 변경사항 없으면 이전 상태 그대로 반환
|
|
if (!hasRemovals && !hasAdditions && !hasUpdates) {
|
|
return prev;
|
|
}
|
|
|
|
let result = filteredTabs.map(tab => {
|
|
const update = updates.find(u => u.key === tab.key);
|
|
return update ? { ...tab, label: update.label } : tab;
|
|
});
|
|
result = [...result, ...newTabs];
|
|
|
|
// 중복 제거
|
|
return result.filter((tab, index, self) =>
|
|
index === self.findIndex(t => t.key === tab.key)
|
|
);
|
|
});
|
|
|
|
// 현재 활성 탭이 삭제된 마스터 항목인 경우 기본 탭으로 전환
|
|
const isNumericKey = /^\d+$/.test(activeAttributeTab);
|
|
if (isNumericKey && !currentFieldIds.has(activeAttributeTab)) {
|
|
setActiveAttributeTab('units');
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [itemMasterFields]);
|
|
|
|
// 메인 탭 핸들러
|
|
const handleAddTab = () => {
|
|
if (!newTabLabel.trim()) {
|
|
toast.error('탭 이름을 입력해주세요');
|
|
return;
|
|
}
|
|
|
|
const newTab: CustomTab = {
|
|
id: Date.now().toString(),
|
|
label: newTabLabel,
|
|
icon: 'FileText',
|
|
isDefault: false,
|
|
order: customTabs.length + 1
|
|
};
|
|
|
|
setCustomTabs(prev => [...prev, newTab]);
|
|
setNewTabLabel('');
|
|
setIsAddTabDialogOpen(false);
|
|
toast.success('탭이 추가되었습니다');
|
|
};
|
|
|
|
const handleUpdateTab = () => {
|
|
if (!newTabLabel.trim() || !editingTabId) {
|
|
toast.error('탭 이름을 입력해주세요');
|
|
return;
|
|
}
|
|
|
|
setCustomTabs(prev => prev.map(tab =>
|
|
tab.id === editingTabId ? { ...tab, label: newTabLabel } : tab
|
|
));
|
|
|
|
setEditingTabId(null);
|
|
setNewTabLabel('');
|
|
setIsAddTabDialogOpen(false);
|
|
setIsManageTabsDialogOpen(true);
|
|
toast.success('탭이 수정되었습니다');
|
|
};
|
|
|
|
const handleDeleteTab = (tabId: string) => {
|
|
const tab = customTabs.find(t => t.id === tabId);
|
|
if (!tab || tab.isDefault) {
|
|
toast.error('기본 탭은 삭제할 수 없습니다');
|
|
return;
|
|
}
|
|
|
|
setDeletingTabId(tabId);
|
|
setIsDeleteTabDialogOpen(true);
|
|
};
|
|
|
|
const confirmDeleteTab = () => {
|
|
if (!deletingTabId) return;
|
|
|
|
setCustomTabs(prev => prev.filter(t => t.id !== deletingTabId));
|
|
if (activeTab === deletingTabId) {
|
|
setActiveTab('hierarchy');
|
|
}
|
|
|
|
setIsDeleteTabDialogOpen(false);
|
|
setDeletingTabId(null);
|
|
toast.success('탭이 삭제되었습니다');
|
|
};
|
|
|
|
// 속성 하위 탭 핸들러
|
|
const handleAddAttributeTab = () => {
|
|
if (!newAttributeTabLabel.trim()) {
|
|
toast.error('탭 이름을 입력해주세요');
|
|
return;
|
|
}
|
|
|
|
const newTab: AttributeSubTab = {
|
|
id: `attr-${Date.now()}`,
|
|
label: newAttributeTabLabel,
|
|
key: `custom-${Date.now()}`,
|
|
isDefault: false,
|
|
order: attributeSubTabs.length
|
|
};
|
|
|
|
setAttributeSubTabs(prev => [...prev, newTab]);
|
|
setNewAttributeTabLabel('');
|
|
setIsAddAttributeTabDialogOpen(false);
|
|
toast.success('속성 탭이 추가되었습니다');
|
|
};
|
|
|
|
const handleUpdateAttributeTab = () => {
|
|
if (!newAttributeTabLabel.trim() || !editingAttributeTabId) {
|
|
toast.error('탭 이름을 입력해주세요');
|
|
return;
|
|
}
|
|
|
|
setAttributeSubTabs(prev => prev.map(tab =>
|
|
tab.id === editingAttributeTabId ? { ...tab, label: newAttributeTabLabel } : tab
|
|
));
|
|
|
|
setEditingAttributeTabId(null);
|
|
setNewAttributeTabLabel('');
|
|
setIsAddAttributeTabDialogOpen(false);
|
|
setIsManageAttributeTabsDialogOpen(true);
|
|
toast.success('속성 탭이 수정되었습니다');
|
|
};
|
|
|
|
const handleDeleteAttributeTab = (tabId: string) => {
|
|
const tab = attributeSubTabs.find(t => t.id === tabId);
|
|
if (!tab || tab.isDefault) {
|
|
toast.error('기본 속성 탭은 삭제할 수 없습니다');
|
|
return;
|
|
}
|
|
|
|
setDeletingAttributeTabId(tabId);
|
|
setIsDeleteAttributeTabDialogOpen(true);
|
|
};
|
|
|
|
const confirmDeleteAttributeTab = () => {
|
|
if (!deletingAttributeTabId) return;
|
|
|
|
setAttributeSubTabs(prev => prev.filter(t => t.id !== deletingAttributeTabId));
|
|
if (activeAttributeTab === deletingAttributeTabId) {
|
|
const firstTab = attributeSubTabs.find(t => t.id !== deletingAttributeTabId);
|
|
if (firstTab) {
|
|
setActiveAttributeTab(firstTab.key);
|
|
}
|
|
}
|
|
|
|
setIsDeleteAttributeTabDialogOpen(false);
|
|
setDeletingAttributeTabId(null);
|
|
toast.success('속성 탭이 삭제되었습니다');
|
|
};
|
|
|
|
// 탭 순서 변경 핸들러
|
|
const moveTabUp = (tabId: string) => {
|
|
const tabIndex = customTabs.findIndex(t => t.id === tabId);
|
|
if (tabIndex <= 0) return;
|
|
|
|
const newTabs = [...customTabs];
|
|
const temp = newTabs[tabIndex - 1].order;
|
|
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
|
newTabs[tabIndex].order = temp;
|
|
|
|
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
|
toast.success('탭 순서가 변경되었습니다');
|
|
};
|
|
|
|
const moveTabDown = (tabId: string) => {
|
|
const tabIndex = customTabs.findIndex(t => t.id === tabId);
|
|
if (tabIndex >= customTabs.length - 1) return;
|
|
|
|
const newTabs = [...customTabs];
|
|
const temp = newTabs[tabIndex + 1].order;
|
|
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
|
newTabs[tabIndex].order = temp;
|
|
|
|
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
|
toast.success('탭 순서가 변경되었습니다');
|
|
};
|
|
|
|
const moveAttributeTabUp = (tabId: string) => {
|
|
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
|
|
if (tabIndex <= 0) return;
|
|
|
|
const newTabs = [...attributeSubTabs];
|
|
const temp = newTabs[tabIndex - 1].order;
|
|
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
|
newTabs[tabIndex].order = temp;
|
|
|
|
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
|
};
|
|
|
|
const moveAttributeTabDown = (tabId: string) => {
|
|
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
|
|
if (tabIndex >= attributeSubTabs.length - 1) return;
|
|
|
|
const newTabs = [...attributeSubTabs];
|
|
const temp = newTabs[tabIndex + 1].order;
|
|
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
|
newTabs[tabIndex].order = temp;
|
|
|
|
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
|
};
|
|
|
|
// 아이콘 헬퍼
|
|
const getTabIcon = (iconName: string) => {
|
|
const icons: Record<string, any> = {
|
|
FolderTree,
|
|
ListTree,
|
|
FileText,
|
|
Settings,
|
|
Layers,
|
|
Database,
|
|
Plus,
|
|
Folder
|
|
};
|
|
return icons[iconName] || FileText;
|
|
};
|
|
|
|
// 탭 관리에서 수정 시작
|
|
const handleEditTabFromManage = (tab: CustomTab) => {
|
|
if (tab.isDefault) {
|
|
toast.error('기본 탭은 수정할 수 없습니다');
|
|
return;
|
|
}
|
|
setEditingTabId(tab.id);
|
|
setNewTabLabel(tab.label);
|
|
setIsManageTabsDialogOpen(false);
|
|
setIsAddTabDialogOpen(true);
|
|
};
|
|
|
|
return {
|
|
// 메인 탭 상태
|
|
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,
|
|
};
|
|
} |