Files
sam-react-prod/src/components/items/ItemMasterDataManagement.tsx.column-manage-backup
byeongcheolryu df3db155dd [feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정
- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리)
- HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가)
- API 클라이언트 구현 (item-master.ts, 13개 엔드포인트)
- ItemMasterContext 구현 (상태 관리 및 데이터 흐름)
- 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등)
- SSR 호환성 수정 (navigator API typeof window 체크)
- 미사용 변수 ESLint 에러 해결
- Context 리팩토링 (AuthContext, RootProvider 추가)
- API 유틸리티 추가 (error-handler, logger, transformers)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 16:10:27 +09:00

3926 lines
174 KiB
Plaintext

'use client';
import { useState, useEffect } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { ItemPage, ItemSection, ItemField, FieldDisplayCondition, ItemMasterField, ItemFieldProperty, SectionTemplate } from '@/contexts/ItemMasterContext';
import { BOMManagementSection, BOMItem } from '@/components/items/BOMManagementSection';
import { CategoryTab, MasterFieldTab, HierarchyTab } from './ItemMasterDataManagement/tabs';
import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog';
import { FieldDrawer } from './ItemMasterDataManagement/dialogs/FieldDrawer';
import { TabManagementDialogs } from './ItemMasterDataManagement/dialogs/TabManagementDialogs';
import { OptionDialog } from './ItemMasterDataManagement/dialogs/OptionDialog';
import { ColumnManageDialog } from './ItemMasterDataManagement/dialogs/ColumnManageDialog';
import {
Database,
Plus,
Trash2,
ChevronDown,
FolderTree,
Folder,
FileText,
Settings,
ListTree,
Save,
X,
GripVertical,
Edit,
Check,
Package,
Layers,
ChevronUp
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
import { toast } from 'sonner';
// 품목분류 로컬스토리지 키
const ITEM_CATEGORIES_KEY = 'item-categories';
const UNIT_OPTIONS_KEY = 'unit-options';
const MATERIAL_OPTIONS_KEY = 'material-options';
const SURFACE_TREATMENT_OPTIONS_KEY = 'surface-treatment-options';
const CUSTOM_ATTRIBUTE_OPTIONS_KEY = 'custom-attribute-options';
// 품목분류 타입
interface ItemCategoryStructure {
[category1: string]: {
[category2: string]: string[];
};
}
// 옵션 칼럼 타입
interface OptionColumn {
id: string;
name: string;
key: string;
type: 'text' | 'number';
required: boolean;
}
// 옵션 타입 (확장된 입력방식 지원)
interface MasterOption {
id: string;
value: string;
label: string;
isActive: boolean;
// 입력 방식 및 속성
inputType?: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
required?: boolean;
options?: string[]; // dropdown일 경우 선택 옵션
defaultValue?: string | number | boolean;
placeholder?: string;
// 기존 칼럼 시스템 (호환성 유지)
columns?: OptionColumn[]; // 칼럼 정의
columnValues?: Record<string, string>; // 칼럼별 값
}
// 초기 데이터
const INITIAL_ITEM_CATEGORIES: ItemCategoryStructure = {
"본체부품": {
"가이드시스템": ["가이드레일"],
"케이스시스템": ["케이스 전면부", "케이스 접검구"],
},
};
const INITIAL_UNIT_OPTIONS: MasterOption[] = [
{ id: 'unit-1', value: 'EA', label: 'EA (개)', isActive: true },
{ id: 'unit-2', value: 'SET', label: 'SET (세트)', isActive: true },
];
const INITIAL_MATERIAL_OPTIONS: MasterOption[] = [
{ id: 'mat-1', value: 'EGI 1.2T', label: 'EGI 1.2T', isActive: true },
{ id: 'mat-2', value: 'SUS 1.2T', label: 'SUS 1.2T', isActive: true },
];
const INITIAL_SURFACE_TREATMENT_OPTIONS: MasterOption[] = [
{ id: 'surf-1', value: '무도장', label: '무도장', isActive: true },
{ id: 'surf-2', value: '파우더도장', label: '파우더도장', isActive: true },
];
const ITEM_TYPE_OPTIONS = [
{ value: 'FG', label: '제품 (FG)' },
{ value: 'PT', label: '부품 (PT)' },
{ value: 'SM', label: '부자재 (SM)' },
{ value: 'RM', label: '원자재 (RM)' },
{ value: 'CS', label: '소모품 (CS)' }
];
const INPUT_TYPE_OPTIONS = [
{ value: 'textbox', label: '텍스트박스' },
{ value: 'dropdown', label: '드롭다운' },
{ value: 'checkbox', label: '체크박스' },
{ value: 'number', label: '숫자' },
{ value: 'date', label: '날짜' },
{ value: 'textarea', label: '텍스트영역' }
];
export function ItemMasterDataManagement() {
const {
itemPages,
addItemPage,
updateItemPage,
deleteItemPage,
addSectionToPage,
updateSection,
deleteSection,
addFieldToSection,
updateField,
deleteField,
reorderFields,
itemMasterFields,
addItemMasterField,
updateItemMasterField,
deleteItemMasterField,
sectionTemplates,
addSectionTemplate,
updateSectionTemplate,
deleteSectionTemplate
} = useItemMaster();
console.log('ItemMasterDataManagement: Current sectionTemplates', sectionTemplates);
// 모든 페이지의 섹션을 하나의 배열로 평탄화
const _itemSections = itemPages.flatMap(page =>
page.sections.map(section => ({
...section,
parentPageId: page.id
}))
);
// 마운트 상태 추적 (SSR 호환)
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 동적 탭 관리 - SSR 호환: 항상 기본값으로 시작
const [customTabs, setCustomTabs] = useState<Array<{id: string; label: string; icon: string; isDefault: boolean; order: number}>>(() => {
return [
{ id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 },
{ id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 },
{ id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 },
{ id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 },
{ id: 'categories', label: '품목분류', icon: 'Folder', isDefault: false, order: 5 }
];
});
// 마운트 후 localStorage에서 탭 로드
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const saved = localStorage.getItem('mes-itemMasterTabs');
if (saved) {
try {
const tabs = JSON.parse(saved);
if (tabs && tabs.length > 0) {
// 품목분류 탭이 없으면 추가
if (!tabs.find((t: any) => t.id === 'categories')) {
tabs.push({ id: 'categories', label: '품목분류', icon: 'Folder', isDefault: false, order: 5 });
}
// 중복 제거
const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) =>
index === self.findIndex((t: any) => t.id === tab.id)
);
setCustomTabs(uniqueTabs);
}
} catch {
// 파싱 실패 시 기본값 유지
}
}
}
}, [mounted]);
// customTabs 변경 시 localStorage에 저장
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const uniqueTabs = customTabs.filter((tab, index, self) =>
index === self.findIndex(t => t.id === tab.id)
);
localStorage.setItem('mes-itemMasterTabs', JSON.stringify(uniqueTabs));
}
}, [customTabs, mounted]);
const [activeTab, setActiveTab] = useState('hierarchy');
// 속성 하위 탭 관리
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<{id: string; label: string; key: string; isDefault: boolean; order: number}>>(() => {
// SSR 호환: 서버 환경에서는 기본값 반환
if (typeof window === 'undefined') {
return [
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
];
}
const saved = localStorage.getItem('mes-attributeSubTabs');
let tabs = [];
if (saved) {
try {
tabs = JSON.parse(saved);
} catch {
tabs = [];
}
}
// 기본값이 없으면 설정
if (!tabs || tabs.length === 0) {
tabs = [
{ id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
{ id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
{ id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
];
}
// 중복 제거 (key 기준 - 실제 데이터 의미 기준)
const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) =>
index === self.findIndex((t: any) => t.key === tab.key)
);
return uniqueTabs;
});
useEffect(() => {
// 저장 전에도 중복 제거 (key 기준 - 실제 데이터 의미 기준)
const uniqueTabs = attributeSubTabs.filter((tab, index, self) =>
index === self.findIndex(t => t.key === tab.key)
);
localStorage.setItem('mes-attributeSubTabs', JSON.stringify(uniqueTabs));
}, [attributeSubTabs]);
// 마스터 항목이 추가/수정될 때 속성 탭 자동 생성
useEffect(() => {
// 새로 추가할 탭들을 먼저 수집
const newTabs: Array<{id: string; label: string; key: string; isDefault: boolean; order: number}> = [];
const updatedTabs: Array<{id: string; label: string; key: string; isDefault: boolean; order: number}> = [];
itemMasterFields.forEach(field => {
// 이미 탭이 있는지 확인
const existingTab = attributeSubTabs.find(tab => tab.key === field.fieldKey);
if (!existingTab) {
// 새로운 탭 추가 대상
const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), ...newTabs.map(t => t.order), -1);
const newTab = {
id: `attr-${field.fieldKey}`,
label: field.name,
key: field.fieldKey,
isDefault: false,
order: maxOrder + 1
};
newTabs.push(newTab);
} else if (existingTab.label !== field.name) {
// 이름이 변경된 경우
updatedTabs.push({ ...existingTab, label: field.name });
}
});
// 상태 업데이트는 한 번만 수행
if (newTabs.length > 0 || updatedTabs.length > 0) {
setAttributeSubTabs(prev => {
// 기존 탭 업데이트
let result = prev.map(tab => {
const updated = updatedTabs.find(ut => ut.key === tab.key);
return updated || tab;
});
// 새 탭 추가
result = [...result, ...newTabs];
// 중복 제거 (key 기준)
const uniqueTabs = result.filter((tab, index, self) =>
index === self.findIndex(t => t.key === tab.key)
);
return uniqueTabs;
});
}
}, [itemMasterFields]);
// 컴포넌트 마운트 시 localStorage 중복 데이터 정리 (한 번만 실행)
useEffect(() => {
const cleanupLocalStorage = () => {
// mes-attributeSubTabs 정리
const savedAttrTabs = localStorage.getItem('mes-attributeSubTabs');
if (savedAttrTabs) {
try {
const tabs = JSON.parse(savedAttrTabs);
const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) =>
index === self.findIndex((t: any) => t.key === tab.key)
);
if (uniqueTabs.length !== tabs.length) {
console.log('🧹 localStorage 중복 제거:', tabs.length - uniqueTabs.length, '개 항목 제거됨');
localStorage.setItem('mes-attributeSubTabs', JSON.stringify(uniqueTabs));
// 상태도 업데이트
setAttributeSubTabs(uniqueTabs);
}
} catch (error) {
console.error('localStorage 정리 중 에러:', error);
}
}
// mes-itemMasterTabs 정리
const savedMainTabs = localStorage.getItem('mes-itemMasterTabs');
if (savedMainTabs) {
try {
const tabs = JSON.parse(savedMainTabs);
const uniqueTabs = tabs.filter((tab: any, index: number, self: any[]) =>
index === self.findIndex((t: any) => t.id === tab.id)
);
if (uniqueTabs.length !== tabs.length) {
console.log('🧹 메인 탭 중복 제거:', tabs.length - uniqueTabs.length, '개 항목 제거됨');
localStorage.setItem('mes-itemMasterTabs', JSON.stringify(uniqueTabs));
setCustomTabs(uniqueTabs);
}
} catch (error) {
console.error('메인 탭 정리 중 에러:', error);
}
}
};
cleanupLocalStorage();
}, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행
const [activeAttributeTab, setActiveAttributeTab] = useState('units');
const [isAddTabDialogOpen, setIsAddTabDialogOpen] = useState(false);
const [isManageTabsDialogOpen, setIsManageTabsDialogOpen] = useState(false);
const [newTabLabel, setNewTabLabel] = useState('');
const [editingTabId, setEditingTabId] = useState<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);
// 품목분류 상태
const [itemCategories, setItemCategories] = useState<ItemCategoryStructure>(() => {
if (typeof window === 'undefined') return INITIAL_ITEM_CATEGORIES;
const saved = localStorage.getItem(ITEM_CATEGORIES_KEY);
return saved ? JSON.parse(saved) : INITIAL_ITEM_CATEGORIES;
});
const [unitOptions, setUnitOptions] = useState<MasterOption[]>(() => {
if (typeof window === 'undefined') return INITIAL_UNIT_OPTIONS;
const saved = localStorage.getItem(UNIT_OPTIONS_KEY);
return saved ? JSON.parse(saved) : INITIAL_UNIT_OPTIONS;
});
const [materialOptions, setMaterialOptions] = useState<MasterOption[]>(() => {
if (typeof window === 'undefined') return INITIAL_MATERIAL_OPTIONS;
const saved = localStorage.getItem(MATERIAL_OPTIONS_KEY);
return saved ? JSON.parse(saved) : INITIAL_MATERIAL_OPTIONS;
});
const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState<MasterOption[]>(() => {
if (typeof window === 'undefined') return INITIAL_SURFACE_TREATMENT_OPTIONS;
const saved = localStorage.getItem(SURFACE_TREATMENT_OPTIONS_KEY);
return saved ? JSON.parse(saved) : INITIAL_SURFACE_TREATMENT_OPTIONS;
});
// 사용자 정의 속성 옵션 상태
const [customAttributeOptions, setCustomAttributeOptions] = useState<Record<string, MasterOption[]>>(() => {
if (typeof window === 'undefined') return {};
const saved = localStorage.getItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY);
return saved ? JSON.parse(saved) : {};
});
const [newCategory1, setNewCategory1] = useState('');
const [newCategory2, setNewCategory2] = useState('');
const [newCategory3, setNewCategory3] = useState('');
const [selectedCategory1, setSelectedCategory1] = useState('');
const [selectedCategory2, setSelectedCategory2] = useState('');
const [isOptionDialogOpen, setIsOptionDialogOpen] = useState(false);
const [editingOptionType, setEditingOptionType] = useState<string | null>(null);
const [newOptionValue, setNewOptionValue] = useState('');
const [newOptionLabel, setNewOptionLabel] = useState('');
const [newOptionColumnValues, setNewOptionColumnValues] = useState<Record<string, string>>({});
// 확장된 입력방식 관련 상태
const [newOptionInputType, setNewOptionInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newOptionRequired, setNewOptionRequired] = useState(false);
const [newOptionOptions, setNewOptionOptions] = useState(''); // dropdown 옵션 (쉼표 구분)
const [newOptionPlaceholder, setNewOptionPlaceholder] = useState('');
const [newOptionDefaultValue, setNewOptionDefaultValue] = useState('');
// 칼럼 관리 상태
const [isColumnManageDialogOpen, setIsColumnManageDialogOpen] = useState(false);
const [managingColumnType, setManagingColumnType] = useState<string | null>(null);
const [attributeColumns, setAttributeColumns] = useState<Record<string, OptionColumn[]>>(() => {
if (typeof window === 'undefined') return {};
const saved = localStorage.getItem('attribute-columns');
return saved ? JSON.parse(saved) : {};
});
// 칼럼 추가 폼 상태
const [newColumnName, setNewColumnName] = useState('');
const [newColumnKey, setNewColumnKey] = useState('');
const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text');
const [newColumnRequired, setNewColumnRequired] = useState(false);
useEffect(() => {
localStorage.setItem('attribute-columns', JSON.stringify(attributeColumns));
}, [attributeColumns]);
// 계층구조 상태
const [selectedPageId, setSelectedPageId] = useState<string | null>(itemPages[0]?.id || null);
const selectedPage = itemPages.find(p => p.id === selectedPageId) || null;
const [_expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
const [editingSectionId, setEditingSectionId] = useState<string | null>(null);
const [editingSectionTitle, setEditingSectionTitle] = useState('');
// 기존 페이지들에 절대경로 자동 생성 (마이그레이션)
useEffect(() => {
let needsUpdate = false;
itemPages.forEach(page => {
if (!page.absolutePath) {
const absolutePath = generateAbsolutePath(page.itemType, page.pageName);
updateItemPage(page.id, { absolutePath });
needsUpdate = true;
}
});
if (needsUpdate) {
console.log('절대경로가 자동으로 생성되었습니다');
}
}, []); // 빈 의존성 배열로 최초 1회만 실행
const [editingPageId, setEditingPageId] = useState<string | null>(null);
const [editingPageName, setEditingPageName] = useState('');
const [isPageDialogOpen, setIsPageDialogOpen] = useState(false);
const [newPageName, setNewPageName] = useState('');
const [newPageItemType, setNewPageItemType] = useState<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>('FG');
const [editingPathPageId, setEditingPathPageId] = useState<string | null>(null);
const [editingAbsolutePath, setEditingAbsolutePath] = useState('');
const [isSectionDialogOpen, setIsSectionDialogOpen] = useState(false);
const [newSectionTitle, setNewSectionTitle] = useState('');
const [newSectionDescription, setNewSectionDescription] = useState('');
const [newSectionType, setNewSectionType] = useState<'fields' | 'bom'>('fields');
// 모바일 체크
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false);
const [selectedSectionForField, setSelectedSectionForField] = useState<string | null>(null);
const [editingFieldId, setEditingFieldId] = useState<string | null>(null);
const [fieldInputMode, setFieldInputMode] = useState<'master' | 'custom'>('custom'); // 마스터 항목 선택 vs 직접 입력
const [showMasterFieldList, setShowMasterFieldList] = useState(false); // 마스터 항목 목록 표시 여부
const [selectedMasterFieldId, setSelectedMasterFieldId] = useState('');
const [newFieldName, setNewFieldName] = useState('');
const [newFieldKey, setNewFieldKey] = useState('');
const [newFieldInputType, setNewFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newFieldRequired, setNewFieldRequired] = useState(false);
const [newFieldOptions, setNewFieldOptions] = useState('');
const [newFieldDescription, setNewFieldDescription] = useState('');
// 텍스트박스 컬럼 관리
const [textboxColumns, setTextboxColumns] = useState<Array<{id: string, name: string, key: string}>>([]);
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
const [editingColumnId, setEditingColumnId] = useState<string | null>(null);
const [columnName, setColumnName] = useState('');
const [columnKey, setColumnKey] = useState('');
// 조건부 항목 상태
const [newFieldConditionEnabled, setNewFieldConditionEnabled] = useState(false);
const [newFieldConditionTargetType, setNewFieldConditionTargetType] = useState<'field' | 'section'>('field');
const [newFieldConditionFields, setNewFieldConditionFields] = useState<Array<{fieldKey: string, expectedValue: string}>>([]);
const [newFieldConditionSections, setNewFieldConditionSections] = useState<string[]>([]);
// 임시 입력용
const [_tempConditionFieldKey, setTempConditionFieldKey] = useState('');
const [tempConditionValue, setTempConditionValue] = useState('');
// 마스터 항목 관리 상태
const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false);
const [editingMasterFieldId, setEditingMasterFieldId] = useState<string | null>(null);
const [newMasterFieldName, setNewMasterFieldName] = useState('');
const [newMasterFieldKey, setNewMasterFieldKey] = useState('');
const [newMasterFieldInputType, setNewMasterFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [newMasterFieldRequired, setNewMasterFieldRequired] = useState(false);
const [newMasterFieldCategory, setNewMasterFieldCategory] = useState('공통');
const [newMasterFieldDescription, setNewMasterFieldDescription] = useState('');
const [newMasterFieldOptions, setNewMasterFieldOptions] = useState('');
const [newMasterFieldAttributeType, setNewMasterFieldAttributeType] = useState<'custom' | 'unit' | 'material' | 'surface'>('custom');
const [newMasterFieldMultiColumn, setNewMasterFieldMultiColumn] = useState(false);
const [newMasterFieldColumnCount, setNewMasterFieldColumnCount] = useState(2);
const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
// 섹션 템플릿 관리 상태
const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false);
const [editingSectionTemplateId, setEditingSectionTemplateId] = useState<string | null>(null);
const [newSectionTemplateTitle, setNewSectionTemplateTitle] = useState('');
const [newSectionTemplateDescription, setNewSectionTemplateDescription] = useState('');
const [newSectionTemplateCategory, setNewSectionTemplateCategory] = useState<string[]>([]);
const [newSectionTemplateType, setNewSectionTemplateType] = useState<'fields' | 'bom'>('fields');
// 섹션 템플릿 불러오기 다이얼로그
const [isLoadTemplateDialogOpen, setIsLoadTemplateDialogOpen] = useState(false);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
// 섹션 템플릿 확장 상태
const [_expandedTemplateId, _setExpandedTemplateId] = useState<string | null>(null);
// 섹션 템플릿 항목 추가 다이얼로그
const [isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState<string | null>(null);
const [editingTemplateFieldId, setEditingTemplateFieldId] = useState<string | null>(null);
const [templateFieldName, setTemplateFieldName] = useState('');
const [templateFieldKey, setTemplateFieldKey] = useState('');
const [templateFieldInputType, setTemplateFieldInputType] = useState<'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'>('textbox');
const [templateFieldRequired, setTemplateFieldRequired] = useState(false);
const [templateFieldOptions, setTemplateFieldOptions] = useState('');
const [templateFieldDescription, setTemplateFieldDescription] = useState('');
const [templateFieldMultiColumn, setTemplateFieldMultiColumn] = useState(false);
const [templateFieldColumnCount, setTemplateFieldColumnCount] = useState(2);
const [templateFieldColumnNames, setTemplateFieldColumnNames] = useState<string[]>(['컬럼1', '컬럼2']);
// 변경사항 추적 상태
const [pendingChanges, setPendingChanges] = useState<{
pages: { id: string; action: 'add' | 'update'; data: any }[];
sections: { id: string; action: 'add' | 'update'; data: any }[];
fields: { id: string; action: 'add' | 'update'; data: any }[];
masterFields: { id: string; action: 'add' | 'update'; data: any }[];
attributes: { id: string; action: 'add' | 'update'; type: string; data: any }[];
}>({
pages: [],
sections: [],
fields: [],
masterFields: [],
attributes: []
});
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// BOM 관리 상태
const [bomItems, setBomItems] = useState<BOMItem[]>(() => {
if (typeof window === 'undefined') return [];
const saved = localStorage.getItem('bom-items');
return saved ? JSON.parse(saved) : [];
});
// BOM 데이터 저장
useEffect(() => {
localStorage.setItem('bom-items', JSON.stringify(bomItems));
}, [bomItems]);
useEffect(() => {
localStorage.setItem(ITEM_CATEGORIES_KEY, JSON.stringify(itemCategories));
}, [itemCategories]);
useEffect(() => {
localStorage.setItem(UNIT_OPTIONS_KEY, JSON.stringify(unitOptions));
}, [unitOptions]);
useEffect(() => {
localStorage.setItem(MATERIAL_OPTIONS_KEY, JSON.stringify(materialOptions));
}, [materialOptions]);
useEffect(() => {
localStorage.setItem(SURFACE_TREATMENT_OPTIONS_KEY, JSON.stringify(surfaceTreatmentOptions));
}, [surfaceTreatmentOptions]);
useEffect(() => {
localStorage.setItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY, JSON.stringify(customAttributeOptions));
}, [customAttributeOptions]);
// 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트
useEffect(() => {
itemMasterFields.forEach(field => {
const attributeType = (field.property as any).attributeType;
if (attributeType && attributeType !== 'custom' && field.property.inputType === 'dropdown') {
let newOptions: string[] = [];
if (attributeType === 'unit') {
newOptions = unitOptions.map(opt => opt.label);
} else if (attributeType === 'material') {
newOptions = materialOptions.map(opt => opt.label);
} else if (attributeType === 'surface') {
newOptions = surfaceTreatmentOptions.map(opt => opt.label);
} else {
// 사용자 정의 속성
const customOptions = customAttributeOptions[attributeType] || [];
newOptions = customOptions.map(opt => opt.label);
}
const currentOptions = field.property.options || [];
const optionsChanged = JSON.stringify(currentOptions.sort()) !== JSON.stringify(newOptions.sort());
if (optionsChanged && newOptions.length > 0) {
updateItemMasterField(field.id, {
...field,
property: {
...field.property,
options: newOptions
}
});
}
}
});
}, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions, itemMasterFields]);
const _handleAddCategory1 = () => {
if (!newCategory1.trim()) return toast.error('대분류명을 입력해주세요');
if (itemCategories[newCategory1]) return toast.error('이미 존재하는 대분류입니다');
setItemCategories({ ...itemCategories, [newCategory1]: {} });
setNewCategory1('');
toast.success('대분류가 추가되었습니다');
};
const _handleAddCategory2 = () => {
if (!selectedCategory1) return toast.error('대분류를 선택해주세요');
if (!newCategory2.trim()) return toast.error('중분류명을 입력해주세요');
setItemCategories({
...itemCategories,
[selectedCategory1]: { ...itemCategories[selectedCategory1], [newCategory2]: [] }
});
setNewCategory2('');
toast.success('중분류가 추가되었습니다');
};
const _handleAddCategory3 = () => {
if (!selectedCategory1 || !selectedCategory2 || !newCategory3.trim())
return toast.error('모든 항목을 입력해주세요');
setItemCategories({
...itemCategories,
[selectedCategory1]: {
...itemCategories[selectedCategory1],
[selectedCategory2]: [...itemCategories[selectedCategory1][selectedCategory2], newCategory3]
}
});
setNewCategory3('');
toast.success('소분류가 추가되었습니다');
};
const _handleDeleteCategory1 = (cat1: string) => {
const newCategories = { ...itemCategories };
delete newCategories[cat1];
setItemCategories(newCategories);
toast.success('삭제되었습니다');
};
const _handleDeleteCategory2 = (cat1: string, cat2: string) => {
const newCategories = { ...itemCategories };
delete newCategories[cat1][cat2];
setItemCategories(newCategories);
toast.success('삭제되었습니다');
};
const _handleDeleteCategory3 = (cat1: string, cat2: string, cat3: string) => {
const newCategories = { ...itemCategories };
newCategories[cat1][cat2] = newCategories[cat1][cat2].filter(c => c !== cat3);
setItemCategories(newCategories);
toast.success('삭제되었습니다');
};
const handleAddOption = () => {
if (!editingOptionType || !newOptionValue.trim() || !newOptionLabel.trim())
return toast.error('모든 항목을 입력해주세요');
// dropdown일 경우 옵션 필수 체크
if (newOptionInputType === 'dropdown' && !newOptionOptions.trim()) {
return toast.error('드롭다운 옵션을 입력해주세요');
}
// 칼럼 필수 값 체크
const currentColumns = attributeColumns[editingOptionType] || [];
for (const column of currentColumns) {
if (column.required && !newOptionColumnValues[column.key]?.trim()) {
return toast.error(`${column.name}은(는) 필수 입력 항목입니다`);
}
}
const newOption: MasterOption = {
id: `${editingOptionType}-${Date.now()}`,
value: newOptionValue,
label: newOptionLabel,
isActive: true,
inputType: newOptionInputType,
required: newOptionRequired,
options: newOptionInputType === 'dropdown' ? newOptionOptions.split(',').map(o => o.trim()).filter(o => o) : undefined,
placeholder: newOptionPlaceholder || undefined,
defaultValue: newOptionDefaultValue || undefined,
columnValues: Object.keys(newOptionColumnValues).length > 0 ? { ...newOptionColumnValues } : undefined
};
if (editingOptionType === 'unit') {
setUnitOptions([...unitOptions, newOption]);
} else if (editingOptionType === 'material') {
setMaterialOptions([...materialOptions, newOption]);
} else if (editingOptionType === 'surface') {
setSurfaceTreatmentOptions([...surfaceTreatmentOptions, newOption]);
} else {
// 사용자 정의 속성 탭
setCustomAttributeOptions(prev => ({
...prev,
[editingOptionType]: [...(prev[editingOptionType] || []), newOption]
}));
}
trackChange('attributes', newOption.id, 'add', newOption, editingOptionType);
setNewOptionValue('');
setNewOptionLabel('');
setNewOptionColumnValues({});
setNewOptionInputType('textbox');
setNewOptionRequired(false);
setNewOptionOptions('');
setNewOptionPlaceholder('');
setNewOptionDefaultValue('');
setIsOptionDialogOpen(false);
toast.success('속성이 추가되었습니다 (저장 필요)');
};
const handleDeleteOption = (type: string, id: string) => {
if (type === 'unit') {
setUnitOptions(unitOptions.filter(o => o.id !== id));
} else if (type === 'material') {
setMaterialOptions(materialOptions.filter(o => o.id !== id));
} else if (type === 'surface') {
setSurfaceTreatmentOptions(surfaceTreatmentOptions.filter(o => o.id !== id));
} else {
// 사용자 정의 속성 탭
setCustomAttributeOptions(prev => ({
...prev,
[type]: (prev[type] || []).filter(o => o.id !== id)
}));
}
toast.success('삭제되었습니다');
};
// 절대경로 자동 생성 함수
const generateAbsolutePath = (itemType: string, pageName: string): string => {
const typeMap: Record<string, string> = {
'FG': '제품관리',
'PT': '부품관리',
'SM': '부자재관리',
'RM': '원자재관리',
'CS': '소모품관리'
};
const category = typeMap[itemType] || '기타';
return `/${category}/${pageName}`;
};
// 계층구조 핸들러
const handleAddPage = () => {
if (!newPageName.trim()) return toast.error('섹션명을 입력해주세요');
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
const newPage: ItemPage = {
id: `PAGE-${Date.now()}`,
pageName: newPageName,
itemType: newPageItemType,
sections: [],
isActive: true,
absolutePath,
createdAt: new Date().toISOString().split('T')[0]
};
addItemPage(newPage);
trackChange('pages', newPage.id, 'add', newPage);
setSelectedPageId(newPage.id);
setNewPageName('');
setIsPageDialogOpen(false);
toast.success('페이지가 추가되었습니다 (저장 필요)');
};
const handleAddSection = () => {
if (!selectedPage || !newSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요');
const newSection: ItemSection = {
id: `SECTION-${Date.now()}`,
title: newSectionTitle,
description: newSectionDescription || undefined,
fields: [],
type: newSectionType,
bomItems: newSectionType === 'bom' ? [] : undefined,
order: selectedPage.sections.length + 1,
isCollapsible: true,
isCollapsed: false,
createdAt: new Date().toISOString().split('T')[0]
};
console.log('Adding section to page:', {
pageId: selectedPage.id,
pageName: selectedPage.pageName,
sectionTitle: newSection.title,
sectionType: newSection.type,
currentSectionCount: selectedPage.sections.length,
newSection: newSection
});
// 1. 페이지에 섹션 추가
addSectionToPage(selectedPage.id, newSection);
trackChange('sections', newSection.id, 'add', newSection);
// 2. 섹션관리 탭에도 템플릿으로 자동 추가
const newTemplate: SectionTemplate = {
id: `TEMPLATE-${Date.now()}`,
title: newSection.title,
description: newSection.description,
category: [selectedPage.itemType], // 현재 페이지의 품목유형을 카테고리로 설정
fields: [], // 초기에는 빈 필드 배열
type: newSection.type,
bomItems: newSection.type === 'bom' ? [] : undefined,
isCollapsible: true,
isCollapsed: false,
isActive: true,
createdAt: new Date().toISOString().split('T')[0]
};
addSectionTemplate(newTemplate);
trackChange('sections', newTemplate.id, 'add', newTemplate);
console.log('Section added to both page and template:', {
sectionId: newSection.id,
templateId: newTemplate.id
});
setNewSectionTitle('');
setNewSectionDescription('');
setNewSectionType('fields');
setIsSectionDialogOpen(false);
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`);
};
const handleEditSectionTitle = (sectionId: string, currentTitle: string) => {
setEditingSectionId(sectionId);
setEditingSectionTitle(currentTitle);
};
const handleSaveSectionTitle = () => {
if (!selectedPage || !editingSectionId || !editingSectionTitle.trim())
return toast.error('하위섹션 제목을 입력해주세요');
updateSection(selectedPage.id, editingSectionId, { title: editingSectionTitle });
trackChange('sections', editingSectionId, 'update', { title: editingSectionTitle });
setEditingSectionId(null);
toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)');
};
const _handleMoveSectionUp = (sectionId: string) => {
if (!selectedPage) return;
const sections = [...selectedPage.sections];
const index = sections.findIndex(s => s.id === sectionId);
if (index <= 0) return; // 첫 번째 섹션이거나 못 찾음
// 배열에서 위치 교환
[sections[index - 1], sections[index]] = [sections[index], sections[index - 1]];
// order 값 재설정
const updatedSections = sections.map((section, idx) => ({
...section,
order: idx + 1
}));
// 페이지 업데이트
updateItemPage(selectedPage.id, { sections: updatedSections });
trackChange('pages', selectedPage.id, 'update', { sections: updatedSections });
toast.success('섹션 순서가 변경되었습니다');
};
const _handleMoveSectionDown = (sectionId: string) => {
if (!selectedPage) return;
const sections = [...selectedPage.sections];
const index = sections.findIndex(s => s.id === sectionId);
if (index < 0 || index >= sections.length - 1) return; // 마지막 섹션이거나 못 찾음
// 배열에서 위치 교환
[sections[index], sections[index + 1]] = [sections[index + 1], sections[index]];
// order 값 재설정
const updatedSections = sections.map((section, idx) => ({
...section,
order: idx + 1
}));
// 페이지 업데이트
updateItemPage(selectedPage.id, { sections: updatedSections });
trackChange('pages', selectedPage.id, 'update', { sections: updatedSections });
toast.success('섹션 순서가 변경되었습니다');
};
const handleAddField = () => {
if (!selectedPage || !selectedSectionForField || !newFieldName.trim() || !newFieldKey.trim())
return toast.error('모든 필수 항목을 입력해주세요');
// 조건부 표시 설정
const displayCondition: FieldDisplayCondition | undefined = newFieldConditionEnabled
? {
targetType: newFieldConditionTargetType,
fieldConditions: newFieldConditionTargetType === 'field' && newFieldConditionFields.length > 0
? newFieldConditionFields
: undefined,
sectionIds: newFieldConditionTargetType === 'section' && newFieldConditionSections.length > 0
? newFieldConditionSections
: undefined
}
: undefined;
// 텍스트박스 컬럼 설정
const hasColumns = newFieldInputType === 'textbox' && textboxColumns.length > 0;
const newField: ItemField = {
id: editingFieldId || `FIELD-${Date.now()}`,
name: newFieldName,
fieldKey: newFieldKey,
property: {
inputType: newFieldInputType,
required: newFieldRequired,
row: 1,
col: 1,
options: newFieldInputType === 'dropdown' && newFieldOptions.trim()
? newFieldOptions.split(',').map(o => o.trim())
: undefined,
multiColumn: hasColumns,
columnCount: hasColumns ? textboxColumns.length : undefined,
columnNames: hasColumns ? textboxColumns.map(c => c.name) : undefined
},
description: newFieldDescription || undefined,
displayCondition,
createdAt: new Date().toISOString().split('T')[0]
};
if (editingFieldId) {
console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.name });
updateField(selectedPage.id, selectedSectionForField, editingFieldId, newField);
trackChange('fields', editingFieldId, 'update', newField);
// 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면)
const existingMasterField = itemMasterFields.find(mf => mf.fieldKey === newField.fieldKey);
if (existingMasterField) {
const updatedMasterField: ItemMasterField = {
...existingMasterField,
name: newField.name,
description: newField.description,
property: newField.property,
displayCondition: newField.displayCondition,
updatedAt: new Date().toISOString().split('T')[0]
};
updateItemMasterField(existingMasterField.id, updatedMasterField);
trackChange('masterFields', existingMasterField.id, 'update', updatedMasterField);
}
toast.success('항목이 섹션에 수정되었습니다!');
} else {
console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.name, fieldKey: newField.fieldKey });
// 1. 섹션에 항목 추가
addFieldToSection(selectedPage.id, selectedSectionForField, newField);
trackChange('fields', newField.id, 'add', newField);
// 2. 항목관리 탭에도 마스터 항목으로 자동 추가 (중복 체크)
const existingMasterField = itemMasterFields.find(mf => mf.fieldKey === newField.fieldKey);
if (!existingMasterField) {
const newMasterField: ItemMasterField = {
id: `MASTER-FIELD-${Date.now()}`,
name: newField.name,
fieldKey: newField.fieldKey,
description: newField.description,
property: newField.property,
category: [selectedPage.itemType], // 현재 페이지의 품목유형을 카테고리로 설정
displayCondition: newField.displayCondition,
isActive: true,
usageCount: 1,
createdAt: new Date().toISOString().split('T')[0]
};
addItemMasterField(newMasterField);
trackChange('masterFields', newMasterField.id, 'add', newMasterField);
console.log('Field added to both section and master fields:', {
fieldId: newField.id,
masterFieldId: newMasterField.id,
fieldKey: newField.fieldKey
});
// 3. dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가
if (newField.property.inputType === 'dropdown' && newField.property.options && newField.property.options.length > 0) {
const existingCustomOptions = customAttributeOptions[newField.fieldKey];
if (!existingCustomOptions || existingCustomOptions.length === 0) {
const customOptions = newField.property.options.map((option, index) => ({
id: `CUSTOM-${newField.fieldKey}-${Date.now()}-${index}`,
value: option,
label: option,
isActive: true
}));
setCustomAttributeOptions(prev => ({
...prev,
[newField.fieldKey]: customOptions
}));
// 속성관리 탭에 하위 탭으로 추가
const existingTab = attributeSubTabs.find(tab => tab.key === newField.fieldKey);
if (!existingTab) {
const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1);
const newTab = {
id: `attr-${newField.fieldKey}`,
label: newField.name,
key: newField.fieldKey,
isDefault: false,
order: maxOrder + 1
};
setAttributeSubTabs(prev => {
// 추가 전 중복 체크 (혹시 모를 race condition 대비)
const alreadyExists = prev.find(t => t.key === newTab.key);
if (alreadyExists) return prev;
const updated = [...prev, newTab];
// 중복 제거
return updated.filter((tab, index, self) =>
index === self.findIndex(t => t.key === tab.key)
);
});
console.log('New attribute tab added:', newTab);
}
console.log('Dropdown options added to custom attributes:', {
attributeKey: newField.fieldKey,
options: customOptions
});
toast.success(`항목이 추가되고 "${newField.name}" 속성 탭이 속성관리에 등록되었습니다!`);
} else {
toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!');
}
} else {
toast.success('항목이 섹션에 추가되고 마스터 항목으로도 등록되었습니다!');
}
} else {
toast.success('항목이 섹션에 추가되었습니다! (이미 마스터 항목에 존재함)');
}
}
// 폼 초기화
setNewFieldName('');
setNewFieldKey('');
setNewFieldInputType('textbox');
setNewFieldRequired(false);
setNewFieldOptions('');
setNewFieldDescription('');
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
setTempConditionFieldKey('');
setTempConditionValue('');
setEditingFieldId(null);
setIsFieldDialogOpen(false);
};
const handleEditField = (sectionId: string, field: ItemField) => {
setSelectedSectionForField(sectionId);
setEditingFieldId(field.id);
setNewFieldName(field.name);
setNewFieldKey(field.fieldKey);
setNewFieldInputType(field.property.inputType);
setNewFieldRequired(field.property.required);
setNewFieldOptions(field.property.options?.join(', ') || '');
setNewFieldDescription(field.description || '');
// 조건부 표시 설정 로드
if (field.displayCondition) {
setNewFieldConditionEnabled(true);
setNewFieldConditionTargetType(field.displayCondition.targetType);
setNewFieldConditionFields(field.displayCondition.fieldConditions || []);
setNewFieldConditionSections(field.displayCondition.sectionIds || []);
} else {
setNewFieldConditionEnabled(false);
setNewFieldConditionTargetType('field');
setNewFieldConditionFields([]);
setNewFieldConditionSections([]);
}
setIsFieldDialogOpen(true);
};
// 마스터 필드 선택 시 폼 자동 채우기
useEffect(() => {
if (fieldInputMode === 'master' && selectedMasterFieldId) {
const masterField = itemMasterFields.find(f => f.id === selectedMasterFieldId);
if (masterField) {
setNewFieldName(masterField.name);
setNewFieldKey(masterField.fieldKey);
setNewFieldInputType(masterField.property.inputType);
setNewFieldRequired(masterField.property.required);
setNewFieldOptions(masterField.property.options?.join(', ') || '');
setNewFieldDescription(masterField.description || '');
}
} else if (fieldInputMode === 'custom') {
// 직접 입력 모드로 전환 시 폼 초기화
setNewFieldName('');
setNewFieldKey('');
setNewFieldInputType('textbox');
setNewFieldRequired(false);
setNewFieldOptions('');
setNewFieldDescription('');
}
}, [fieldInputMode, selectedMasterFieldId, itemMasterFields]);
// 마스터 항목 관리 핸들러
const handleAddMasterField = () => {
if (!newMasterFieldName.trim() || !newMasterFieldKey.trim())
return toast.error('항목명과 필드 키를 입력해주세요');
// 속성 목록 초기화 (dropdown 타입이고 옵션이 있으면 각 옵션을 속성으로 추가)
let properties: ItemFieldProperty[] = [];
if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) {
const options = newMasterFieldOptions.split(',').map(o => o.trim());
properties = options.map((opt, idx) => ({
id: `prop-${Date.now()}-${idx}`,
key: `${newMasterFieldKey}_${opt.toLowerCase().replace(/\s+/g, '_')}`,
label: opt,
type: 'textbox',
inputType: 'textbox',
required: false,
row: idx + 1,
col: 1
}));
}
const newMasterField: ItemMasterField = {
id: `MASTER-${Date.now()}`,
name: newMasterFieldName,
fieldKey: newMasterFieldKey,
property: {
inputType: newMasterFieldInputType,
required: newMasterFieldRequired,
row: 1,
col: 1,
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
? newMasterFieldOptions.split(',').map(o => o.trim())
: undefined,
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
} as any,
properties: properties.length > 0 ? properties : undefined,
category: newMasterFieldCategory,
description: newMasterFieldDescription || undefined,
isActive: true,
createdAt: new Date().toISOString().split('T')[0]
};
addItemMasterField(newMasterField);
trackChange('masterFields', newMasterField.id, 'add', newMasterField);
// dropdown 타입이고 attributeType이 'custom'이며 옵션이 있으면 속성관리 탭에도 자동 추가
if (newMasterFieldInputType === 'dropdown' && newMasterFieldAttributeType === 'custom' && newMasterFieldOptions.trim()) {
const options = newMasterFieldOptions.split(',').map(o => o.trim());
const existingCustomOptions = customAttributeOptions[newMasterFieldKey];
if (!existingCustomOptions || existingCustomOptions.length === 0) {
const customOptions = options.map((option, index) => ({
id: `CUSTOM-${newMasterFieldKey}-${Date.now()}-${index}`,
value: option,
label: option,
isActive: true
}));
setCustomAttributeOptions(prev => ({
...prev,
[newMasterFieldKey]: customOptions
}));
// 속성관리 탭에 하위 탭으로 추가
const existingTab = attributeSubTabs.find(tab => tab.key === newMasterFieldKey);
if (!existingTab) {
const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1);
const newTab = {
id: `attr-${newMasterFieldKey}`,
label: newMasterFieldName,
key: newMasterFieldKey,
isDefault: false,
order: maxOrder + 1
};
setAttributeSubTabs(prev => {
// 추가 전 중복 체크 (혹시 모를 race condition 대비)
const alreadyExists = prev.find(t => t.key === newTab.key);
if (alreadyExists) return prev;
const updated = [...prev, newTab];
// 중복 제거
return updated.filter((tab, index, self) =>
index === self.findIndex(t => t.key === tab.key)
);
});
console.log('New attribute tab added from master field:', newTab);
}
console.log('Master field dropdown options added to custom attributes:', {
attributeKey: newMasterFieldKey,
options: customOptions
});
}
}
// 폼 초기화
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
setIsMasterFieldDialogOpen(false);
toast.success('마스터 항목이 추가되었습니다 (속성 탭에 반영됨, 저장 필요)');
};
const handleEditMasterField = (field: ItemMasterField) => {
setEditingMasterFieldId(field.id);
setNewMasterFieldName(field.name);
setNewMasterFieldKey(field.fieldKey);
setNewMasterFieldInputType(field.property.inputType);
setNewMasterFieldRequired(field.property.required);
setNewMasterFieldCategory(field.category || '공통');
setNewMasterFieldDescription(field.description || '');
setNewMasterFieldOptions(field.property.options?.join(', ') || '');
setNewMasterFieldAttributeType((field.property as any).attributeType || 'custom');
setNewMasterFieldMultiColumn(field.property.multiColumn || false);
setNewMasterFieldColumnCount(field.property.columnCount || 2);
setNewMasterFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']);
setIsMasterFieldDialogOpen(true);
};
const handleUpdateMasterField = () => {
if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim())
return toast.error('항목명과 필드 키를 입력해주세요');
// 속성 목록 업데이트 (dropdown 타입이고 옵션이 있으면 각 옵션을 속성으로 추가)
let properties: ItemFieldProperty[] = [];
if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) {
const options = newMasterFieldOptions.split(',').map(o => o.trim());
properties = options.map((opt, idx) => ({
id: `prop-${Date.now()}-${idx}`,
key: `${newMasterFieldKey}_${opt.toLowerCase().replace(/\s+/g, '_')}`,
label: opt,
type: 'textbox',
inputType: 'textbox',
required: false,
row: idx + 1,
col: 1
}));
}
updateItemMasterField(editingMasterFieldId, {
name: newMasterFieldName,
fieldKey: newMasterFieldKey,
property: {
inputType: newMasterFieldInputType,
required: newMasterFieldRequired,
row: 1,
col: 1,
options: newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()
? newMasterFieldOptions.split(',').map(o => o.trim())
: undefined,
attributeType: newMasterFieldInputType === 'dropdown' ? newMasterFieldAttributeType : undefined,
multiColumn: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') ? newMasterFieldMultiColumn : undefined,
columnCount: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnCount : undefined,
columnNames: (newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && newMasterFieldMultiColumn ? newMasterFieldColumnNames : undefined
} as any,
properties: properties.length > 0 ? properties : undefined,
category: newMasterFieldCategory,
description: newMasterFieldDescription || undefined
});
// 폼 초기화
setEditingMasterFieldId(null);
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
setIsMasterFieldDialogOpen(false);
trackChange('masterFields', editingMasterFieldId, 'update', { id: editingMasterFieldId });
toast.success('마스터 항목이 수정되었습니다 (속성 탭에 반영됨, 저장 필요)');
};
const handleDeleteMasterField = (id: string) => {
if (confirm('이 마스터 항목을 삭제하시겠습니까?')) {
// 삭제할 마스터 항목 찾기
const fieldToDelete = itemMasterFields.find(f => f.id === id);
// 마스터 항목 삭제
deleteItemMasterField(id);
// 속성 탭에서 해당 탭 제거
if (fieldToDelete) {
setAttributeSubTabs(prev => prev.filter(tab => tab.key !== fieldToDelete.fieldKey));
// 삭제된 탭이 현재 활성 탭이면 다른 탭으로 전환
if (activeAttributeTab === fieldToDelete.fieldKey) {
setActiveAttributeTab('units');
}
}
toast.success('마스터 항목이 삭제되었습니다');
}
};
// 섹션 템플릿 핸들러
const handleAddSectionTemplate = () => {
if (!newSectionTemplateTitle.trim())
return toast.error('섹션 제목을 입력해주세요');
const newTemplate: SectionTemplate = {
id: `TEMPLATE-${Date.now()}`,
title: newSectionTemplateTitle,
description: newSectionTemplateDescription || undefined,
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
fields: [],
type: newSectionTemplateType,
bomItems: newSectionTemplateType === 'bom' ? [] : undefined,
isCollapsible: true,
isCollapsed: false,
isActive: true,
createdAt: new Date().toISOString().split('T')[0]
};
console.log('Adding section template:', newTemplate);
addSectionTemplate(newTemplate);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
setIsSectionTemplateDialogOpen(false);
toast.success('섹션 템플릿이 추가되었습니다! (템플릿 목록에서 확인 가능)');
};
const handleEditSectionTemplate = (template: SectionTemplate) => {
setEditingSectionTemplateId(template.id);
setNewSectionTemplateTitle(template.title);
setNewSectionTemplateDescription(template.description || '');
setNewSectionTemplateCategory(template.category || []);
setNewSectionTemplateType(template.type || 'fields');
setIsSectionTemplateDialogOpen(true);
};
const handleUpdateSectionTemplate = () => {
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim())
return toast.error('섹션 제목을 입력해주세요');
updateSectionTemplate(editingSectionTemplateId, {
title: newSectionTemplateTitle,
description: newSectionTemplateDescription || undefined,
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
type: newSectionTemplateType
});
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
setIsSectionTemplateDialogOpen(false);
toast.success('섹션이 수정되었습니다');
};
const handleDeleteSectionTemplate = (id: string) => {
if (confirm('이 섹션을 삭제하시겠습니까?')) {
deleteSectionTemplate(id);
toast.success('섹션이 삭제되었습니다');
}
};
// 섹션 템플릿 불러오기
const handleLoadTemplate = () => {
if (!selectedTemplateId || !selectedPage) {
return toast.error('템플릿을 선택해주세요');
}
const template = sectionTemplates.find(t => t.id === selectedTemplateId);
if (!template) {
return toast.error('템플릿을 찾을 수 없습니다');
}
// 템플릿을 복사해서 섹션으로 추가
const newSection: ItemSection = {
id: `SECTION-${Date.now()}`,
title: template.title,
description: template.description,
category: template.category,
fields: template.fields.map(field => ({
...field,
id: `FIELD-${Date.now()}-${Math.random()}`,
createdAt: new Date().toISOString().split('T')[0]
})),
type: template.type,
bomItems: template.type === 'bom' && template.bomItems ? template.bomItems.map(bom => ({
...bom,
id: `BOM-${Date.now()}-${Math.random()}`,
createdAt: new Date().toISOString().split('T')[0]
})) : undefined,
order: selectedPage.sections.length + 1,
isCollapsible: template.isCollapsible,
isCollapsed: template.isCollapsed,
createdAt: new Date().toISOString().split('T')[0]
};
console.log('Loading template to section:', template.title, 'type:', template.type, 'newSection:', newSection);
addSectionToPage(selectedPage.id, newSection);
setSelectedTemplateId(null);
setIsLoadTemplateDialogOpen(false);
toast.success('섹션이 불러와졌습니다');
};
// 섹션 템플릿 항목 추가
const handleAddTemplateField = () => {
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
return toast.error('모든 필수 항목을 입력해주세요');
}
const template = sectionTemplates.find(t => t.id === currentTemplateId);
if (!template) return;
// 항목 탭에 해당 항목이 없으면 자동으로 추가
const existingMasterField = itemMasterFields.find(f => f.fieldKey === templateFieldKey);
if (!existingMasterField && !editingTemplateFieldId) {
const newMasterField: ItemMasterField = {
id: `MASTER-${Date.now()}`,
name: templateFieldName,
fieldKey: templateFieldKey,
property: {
inputType: templateFieldInputType,
required: templateFieldRequired,
row: 1,
col: 1,
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
? templateFieldOptions.split(',').map(o => o.trim())
: undefined,
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
} as any,
category: '공통',
description: templateFieldDescription || undefined,
isActive: true,
createdAt: new Date().toISOString().split('T')[0]
};
addItemMasterField(newMasterField);
// dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가
if (templateFieldInputType === 'dropdown' && templateFieldOptions.trim()) {
const options = templateFieldOptions.split(',').map(o => o.trim());
const existingCustomOptions = customAttributeOptions[templateFieldKey];
if (!existingCustomOptions || existingCustomOptions.length === 0) {
const customOptions = options.map((option, index) => ({
id: `CUSTOM-${templateFieldKey}-${Date.now()}-${index}`,
value: option,
label: option,
isActive: true
}));
setCustomAttributeOptions(prev => ({
...prev,
[templateFieldKey]: customOptions
}));
console.log('Template field dropdown options added to custom attributes:', {
attributeKey: templateFieldKey,
options: customOptions
});
toast.success(`항목 탭과 속성관리 탭에 "${templateFieldName}" 속성이 자동으로 추가되었습니다`);
} else {
toast.success('항목 탭에 자동으로 추가되었습니다');
}
} else {
toast.success('항목 탭에 자동으로 추가되었습니다');
}
}
const newField: ItemField = {
id: editingTemplateFieldId || `FIELD-${Date.now()}`,
name: templateFieldName,
fieldKey: templateFieldKey,
property: {
inputType: templateFieldInputType,
required: templateFieldRequired,
row: 1,
col: 1,
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
? templateFieldOptions.split(',').map(o => o.trim())
: undefined,
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
},
description: templateFieldDescription || undefined,
createdAt: new Date().toISOString().split('T')[0]
};
let updatedFields;
if (editingTemplateFieldId) {
updatedFields = template.fields.map(f => f.id === editingTemplateFieldId ? newField : f);
toast.success('항목이 수정되었습니다');
} else {
updatedFields = [...template.fields, newField];
toast.success('항목이 추가되었습니다');
}
updateSectionTemplate(currentTemplateId, { fields: updatedFields });
// 폼 초기화
setTemplateFieldName('');
setTemplateFieldKey('');
setTemplateFieldInputType('textbox');
setTemplateFieldRequired(false);
setTemplateFieldOptions('');
setTemplateFieldDescription('');
setTemplateFieldMultiColumn(false);
setTemplateFieldColumnCount(2);
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
setEditingTemplateFieldId(null);
setIsTemplateFieldDialogOpen(false);
};
const handleEditTemplateField = (templateId: string, field: ItemField) => {
setCurrentTemplateId(templateId);
setEditingTemplateFieldId(field.id);
setTemplateFieldName(field.name);
setTemplateFieldKey(field.fieldKey);
setTemplateFieldInputType(field.property.inputType);
setTemplateFieldRequired(field.property.required);
setTemplateFieldOptions(field.property.options?.join(', ') || '');
setTemplateFieldDescription(field.description || '');
setTemplateFieldMultiColumn(field.property.multiColumn || false);
setTemplateFieldColumnCount(field.property.columnCount || 2);
setTemplateFieldColumnNames(field.property.columnNames || ['컬럼1', '컬럼2']);
setIsTemplateFieldDialogOpen(true);
};
const handleDeleteTemplateField = (templateId: string, fieldId: string) => {
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
const template = sectionTemplates.find(t => t.id === templateId);
if (!template) return;
const updatedFields = template.fields.filter(f => f.id !== fieldId);
updateSectionTemplate(templateId, { fields: updatedFields });
toast.success('항목이 삭제되었습니다');
};
// BOM 관리 핸들러
const _handleAddBOMItem = (item: Omit<BOMItem, 'id' | 'createdAt'>) => {
const newItem: BOMItem = {
...item,
id: `BOM-${Date.now()}`,
createdAt: new Date().toISOString().split('T')[0]
};
setBomItems(prev => [...prev, newItem]);
};
const _handleUpdateBOMItem = (id: string, item: Partial<BOMItem>) => {
setBomItems(prev => prev.map(bom => bom.id === id ? { ...bom, ...item } : bom));
};
const _handleDeleteBOMItem = (id: string) => {
setBomItems(prev => prev.filter(bom => bom.id !== id));
};
// 템플릿별 BOM 관리 핸들러
const handleAddBOMItemToTemplate = (templateId: string, item: Omit<BOMItem, 'id' | 'createdAt'>) => {
const newItem: BOMItem = {
...item,
id: `BOM-${Date.now()}`,
createdAt: new Date().toISOString().split('T')[0]
};
const template = sectionTemplates.find(t => t.id === templateId);
if (!template) return;
const updatedBomItems = [...(template.bomItems || []), newItem];
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
const handleUpdateBOMItemInTemplate = (templateId: string, itemId: string, item: Partial<BOMItem>) => {
const template = sectionTemplates.find(t => t.id === templateId);
if (!template || !template.bomItems) return;
const updatedBomItems = template.bomItems.map(bom =>
bom.id === itemId ? { ...bom, ...item } : bom
);
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
const handleDeleteBOMItemFromTemplate = (templateId: string, itemId: string) => {
const template = sectionTemplates.find(t => t.id === templateId);
if (!template || !template.bomItems) return;
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
};
const _toggleSection = (sectionId: string) => {
setExpandedSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
};
// 탭 관리 함수
const handleAddTab = () => {
if (!newTabLabel.trim()) return toast.error('탭 이름을 입력해주세요');
const newTab = {
id: `TAB-${Date.now()}`,
label: newTabLabel,
icon: 'FileText',
isDefault: false,
order: customTabs.length + 1
};
setCustomTabs(prev => [...prev, newTab]);
setNewTabLabel('');
setIsAddTabDialogOpen(false);
toast.success('탭이 추가되었습니다');
};
const _handleEditTab = (tabId: string) => {
const tab = customTabs.find(t => t.id === tabId);
if (!tab || tab.isDefault) return;
setEditingTabId(tabId);
setNewTabLabel(tab.label);
setIsAddTabDialogOpen(true);
};
const handleUpdateTab = () => {
if (!newTabLabel.trim() || !editingTabId) return toast.error('탭 이름을 입력해주세요');
setCustomTabs(prev => prev.map(tab =>
tab.id === editingTabId ? { ...tab, label: newTabLabel } : tab
));
setEditingTabId(null);
setNewTabLabel('');
setIsAddTabDialogOpen(false);
setIsManageTabsDialogOpen(true);
toast.success('탭이 수정되었습니다');
};
const handleDeleteTab = (tabId: string) => {
const tab = customTabs.find(t => t.id === tabId);
if (!tab || tab.isDefault) return toast.error('기본 탭은 삭제할 수 없습니다');
setDeletingTabId(tabId);
setIsDeleteTabDialogOpen(true);
};
const confirmDeleteTab = () => {
if (!deletingTabId) return;
setCustomTabs(prev => prev.filter(t => t.id !== deletingTabId));
if (activeTab === deletingTabId) setActiveTab('categories');
setIsDeleteTabDialogOpen(false);
setDeletingTabId(null);
toast.success('탭이 삭제되었습니다');
};
// 속성 하위 탭 관리 함수들
const handleAddAttributeTab = () => {
if (!newAttributeTabLabel.trim()) return toast.error('탭 이름을 입력해주세요');
const newTab = {
id: `attr-${Date.now()}`,
label: newAttributeTabLabel,
key: `custom-${Date.now()}`,
isDefault: false,
order: attributeSubTabs.length
};
setAttributeSubTabs(prev => [...prev, newTab]);
setNewAttributeTabLabel('');
setIsAddAttributeTabDialogOpen(false);
toast.success('속성 탭이 추가되었습니다');
};
const handleUpdateAttributeTab = () => {
if (!newAttributeTabLabel.trim() || !editingAttributeTabId) return toast.error('탭 이름을 입력해주세요');
setAttributeSubTabs(prev => prev.map(tab =>
tab.id === editingAttributeTabId ? { ...tab, label: newAttributeTabLabel } : tab
));
setEditingAttributeTabId(null);
setNewAttributeTabLabel('');
setIsAddAttributeTabDialogOpen(false);
setIsManageAttributeTabsDialogOpen(true);
toast.success('속성 탭이 수정되었습니다');
};
const handleDeleteAttributeTab = (tabId: string) => {
const tab = attributeSubTabs.find(t => t.id === tabId);
if (!tab || tab.isDefault) return toast.error('기본 속성 탭은 삭제할 수 없습니다');
setDeletingAttributeTabId(tabId);
setIsDeleteAttributeTabDialogOpen(true);
};
const confirmDeleteAttributeTab = () => {
if (!deletingAttributeTabId) return;
setAttributeSubTabs(prev => prev.filter(t => t.id !== deletingAttributeTabId));
if (activeAttributeTab === deletingAttributeTabId) {
const firstTab = attributeSubTabs.find(t => t.id !== deletingAttributeTabId);
if (firstTab) setActiveAttributeTab(firstTab.key);
}
setIsDeleteAttributeTabDialogOpen(false);
setDeletingAttributeTabId(null);
toast.success('속성 탭이 삭제되었습니다');
};
const moveAttributeTabUp = (tabId: string) => {
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
if (tabIndex <= 0) return;
const newTabs = [...attributeSubTabs];
const temp = newTabs[tabIndex - 1].order;
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
};
const moveAttributeTabDown = (tabId: string) => {
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
if (tabIndex >= attributeSubTabs.length - 1) return;
const newTabs = [...attributeSubTabs];
const temp = newTabs[tabIndex + 1].order;
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
};
const getTabIcon = (iconName: string) => {
const icons: Record<string, any> = {
FolderTree, ListTree, FileText, Settings, Layers, Database, Plus, Folder
};
return icons[iconName] || FileText;
};
const moveTabUp = (tabId: string) => {
const tabIndex = customTabs.findIndex(t => t.id === tabId);
if (tabIndex <= 0) return;
const newTabs = [...customTabs];
const temp = newTabs[tabIndex - 1].order;
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
toast.success('탭 순서가 변경되었습니다');
};
const moveTabDown = (tabId: string) => {
const tabIndex = customTabs.findIndex(t => t.id === tabId);
if (tabIndex >= customTabs.length - 1) return;
const newTabs = [...customTabs];
const temp = newTabs[tabIndex + 1].order;
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
newTabs[tabIndex].order = temp;
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
toast.success('탭 순서가 변경되었습니다');
};
const handleEditTabFromManage = (tab: typeof customTabs[0]) => {
if (tab.isDefault) return toast.error('기본 탭은 수정할 수 없습니다');
setEditingTabId(tab.id);
setNewTabLabel(tab.label);
setIsManageTabsDialogOpen(false);
setIsAddTabDialogOpen(true);
};
// 현재 섹션의 모든 필드 가져오기 (조건부 필드 참조용)
const _getAllFieldsInSection = (sectionId: string) => {
if (!selectedPage) return [];
const section = selectedPage.sections.find(s => s.id === sectionId);
return section?.fields || [];
};
// 섹션 순서 변경 핸들러 (드래그앤드롭)
const moveSection = (dragIndex: number, hoverIndex: number) => {
if (!selectedPage) return;
const sections = [...selectedPage.sections];
const [draggedSection] = sections.splice(dragIndex, 1);
sections.splice(hoverIndex, 0, draggedSection);
// order 값 재설정
const updatedSections = sections.map((section, idx) => ({
...section,
order: idx + 1
}));
// 페이지 업데이트
updateItemPage(selectedPage.id, { sections: updatedSections });
trackChange('pages', selectedPage.id, 'update', { sections: updatedSections });
toast.success('섹션 순서가 변경되었습니다 (저장 필요)');
};
// 필드 순서 변경 핸들러
const moveField = (sectionId: string, dragIndex: number, hoverIndex: number) => {
if (!selectedPage) return;
const section = selectedPage.sections.find(s => s.id === sectionId);
if (!section) return;
const newFields = [...section.fields];
const [draggedField] = newFields.splice(dragIndex, 1);
newFields.splice(hoverIndex, 0, draggedField);
reorderFields(selectedPage.id, sectionId, newFields.map(f => f.id));
};
// 변경사항 추적 함수
const trackChange = (type: 'pages' | 'sections' | 'fields' | 'masterFields' | 'attributes', id: string, action: 'add' | 'update', data: any, attributeType?: string) => {
setPendingChanges(prev => {
const updated = { ...prev };
if (type === 'attributes') {
const existingIndex = updated.attributes.findIndex(item => item.id === id);
if (existingIndex >= 0) {
updated.attributes[existingIndex] = { id, action, type: attributeType || '', data };
} else {
updated.attributes.push({ id, action, type: attributeType || '', data });
}
} else {
const existingIndex = updated[type].findIndex(item => item.id === id);
if (existingIndex >= 0) {
updated[type][existingIndex] = { id, action, data };
} else {
updated[type].push({ id, action, data });
}
}
return updated;
});
setHasUnsavedChanges(true);
};
// 일괄 저장 핸들러 - 모든 페이지, 섹션, 항목, 속성을 통합 저장
const handleSaveAllChanges = () => {
if (!hasUnsavedChanges) {
return toast.info('저장할 변경사항이 없습니다');
}
try {
// 모든 변경사항은 이미 DataContext에 실시간으로 반영되어 있습니다.
// DataContext의 useEffect가 자동으로 localStorage에 저장합니다.
// 이 함수는 변경사항 추적을 초기화하고 사용자에게 확인 메시지를 보여줍니다.
const totalChanges =
pendingChanges.pages.length +
pendingChanges.sections.length +
pendingChanges.fields.length +
pendingChanges.masterFields.length +
pendingChanges.attributes.length;
// 변경사항 요약
const summary = [];
if (pendingChanges.pages.length > 0) summary.push(`페이지 ${pendingChanges.pages.length}개`);
if (pendingChanges.sections.length > 0) summary.push(`섹션 ${pendingChanges.sections.length}개`);
if (pendingChanges.fields.length > 0) summary.push(`항목 ${pendingChanges.fields.length}개`);
if (pendingChanges.masterFields.length > 0) summary.push(`마스터항목 ${pendingChanges.masterFields.length}개`);
if (pendingChanges.attributes.length > 0) summary.push(`속성 ${pendingChanges.attributes.length}개`);
console.log('Confirming changes:', { totalChanges, pendingChanges, currentItemPages: itemPages });
// itemPages, sectionTemplates, itemMasterFields를 명시적으로 localStorage에 저장 (안전성 보장)
localStorage.setItem('mes-itemPages', JSON.stringify(itemPages));
localStorage.setItem('mes-sectionTemplates', JSON.stringify(sectionTemplates));
localStorage.setItem('mes-itemMasterFields', JSON.stringify(itemMasterFields));
console.log('Saved to localStorage:', {
itemPages: itemPages.length,
sectionTemplates: sectionTemplates.length,
itemMasterFields: itemMasterFields.length
});
// 속성 탭의 모든 데이터도 localStorage에 명시적으로 저장 (안전성 보장)
localStorage.setItem(UNIT_OPTIONS_KEY, JSON.stringify(unitOptions));
localStorage.setItem(MATERIAL_OPTIONS_KEY, JSON.stringify(materialOptions));
localStorage.setItem(SURFACE_TREATMENT_OPTIONS_KEY, JSON.stringify(surfaceTreatmentOptions));
localStorage.setItem(CUSTOM_ATTRIBUTE_OPTIONS_KEY, JSON.stringify(customAttributeOptions));
localStorage.setItem(ITEM_CATEGORIES_KEY, JSON.stringify(itemCategories));
// 변경사항 초기화
setPendingChanges({
pages: [],
sections: [],
fields: [],
masterFields: [],
attributes: []
});
setHasUnsavedChanges(false);
toast.success(`✅ 모든 변경사항 저장 완료!\n${summary.join(', ')} - 총 ${totalChanges}건\n페이지, 섹션, 항목, 속성이 모두 자동 목록에 반영되었습니다.`);
} catch (error) {
toast.error('저장 중 오류가 발생했습니다');
console.error('Save error:', error);
}
};
return (
<PageLayout>
<PageHeader
title="품목기준관리"
description="품목관리에서 사용되는 기준 정보를 설정하고 관리합니다"
icon={Database}
/>
{/* 전역 저장 버튼 - 모든 탭의 변경사항을 저장 */}
{hasUnsavedChanges && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Badge variant="destructive" className="animate-pulse text-base px-3 py-1">
{pendingChanges.pages.length +
pendingChanges.sections.length +
pendingChanges.fields.length +
pendingChanges.masterFields.length +
pendingChanges.attributes.length}개 변경사항
</Badge>
<div className="text-sm text-gray-700">
{pendingChanges.pages.length > 0 && <span className="mr-2">• 페이지 {pendingChanges.pages.length}개</span>}
{pendingChanges.sections.length > 0 && <span className="mr-2">• 섹션 {pendingChanges.sections.length}개</span>}
{pendingChanges.fields.length > 0 && <span className="mr-2">• 항목 {pendingChanges.fields.length}개</span>}
{pendingChanges.masterFields.length > 0 && <span className="mr-2">• 마스터항목 {pendingChanges.masterFields.length}개</span>}
{pendingChanges.attributes.length > 0 && <span className="mr-2">• 속성 {pendingChanges.attributes.length}개</span>}
</div>
</div>
<Button
size="lg"
variant="default"
onClick={handleSaveAllChanges}
className="bg-green-600 hover:bg-green-700 shadow-lg"
>
<Save className="h-5 w-5 mr-2" />
모든 변경사항 저장
</Button>
</div>
</div>
)}
<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>
</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;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
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('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;
const inputTypeLabel =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
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('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 =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
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.fieldKey === currentTabKey);
// 마스터 항목이면 해당 항목의 속성값들을 표시
if (masterField && masterField.properties && masterField.properties.length > 0) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-medium">{masterField.name} 속성 목록</h3>
<p className="text-sm text-muted-foreground mt-1">
항목 탭에서 추가한 "{masterField.name}" 항목의 속성값들입니다
</p>
</div>
</div>
<div className="space-y-3">
{masterField.properties.map((property) => {
const inputTypeLabel =
property.type === 'textbox' ? '텍스트박스' :
property.type === 'number' ? '숫자' :
property.type === 'dropdown' ? '드롭다운' :
property.type === 'checkbox' ? '체크박스' :
property.type === 'date' ? '날짜' :
property.type === 'textarea' ? '텍스트영역' :
'텍스트박스';
return (
<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, idx) => (
<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.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 =
option.inputType === 'textbox' ? '텍스트박스' :
option.inputType === 'number' ? '숫자' :
option.inputType === 'dropdown' ? '드롭다운' :
option.inputType === 'checkbox' ? '체크박스' :
option.inputType === 'date' ? '날짜' :
option.inputType === 'textarea' ? '텍스트영역' :
'텍스트박스';
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={hasUnsavedChanges}
pendingChanges={pendingChanges}
/>
</TabsContent>
{/* 섹션관리 탭 */}
<TabsContent value="sections" className="space-y-4">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>섹션관리</CardTitle>
<CardDescription>재사용 가능한 섹션 템플릿을 관리합니다</CardDescription>
</div>
<Button onClick={() => setIsSectionTemplateDialogOpen(true)}>
<Plus className="h-4 w-4 mr-2" />섹션추가
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="general" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="general" className="flex items-center gap-2">
<Folder className="h-4 w-4" />
일반 섹션
</TabsTrigger>
<TabsTrigger value="module" className="flex items-center gap-2">
<Package className="h-4 w-4" />
모듈 섹션
</TabsTrigger>
</TabsList>
{/* 일반 섹션 탭 */}
<TabsContent value="general">
{(() => {
console.log('Rendering section templates:', {
totalTemplates: sectionTemplates.length,
generalTemplates: sectionTemplates.filter(t => t.type !== 'bom').length,
templates: sectionTemplates.map(t => ({ id: t.id, title: t.title, type: t.type }))
});
return null;
})()}
{sectionTemplates.filter(t => t.type !== 'bom').length === 0 ? (
<div className="text-center py-12">
<Folder className="w-16 h-16 mx-auto text-gray-300 mb-4" />
<p className="text-muted-foreground mb-2">등록된 일반 섹션이 없습니다</p>
<p className="text-sm text-muted-foreground">
섹션추가 버튼을 눌러 재사용 가능한 섹션을 등록하세요.
</p>
</div>
) : (
<div className="space-y-4">
{sectionTemplates.filter(t => t.type !== 'bom').map((template) => (
<Card key={template.id}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<Folder className="h-5 w-5 text-blue-500" />
<div className="flex-1">
<CardTitle className="text-base">{template.title}</CardTitle>
{template.description && (
<CardDescription className="text-sm mt-0.5">{template.description}</CardDescription>
)}
</div>
</div>
<div className="flex items-center gap-2">
{template.category && template.category.length > 0 && (
<div className="flex flex-wrap gap-1 mr-2">
{template.category.map((cat, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat}
</Badge>
))}
</div>
)}
<Button
size="sm"
variant="ghost"
onClick={() => handleEditSectionTemplate(template)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteSectionTemplate(template.id)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
이 템플릿과 관련되는 항목 목록을 조회합니다
</p>
<Button
size="sm"
onClick={() => {
setCurrentTemplateId(template.id);
setIsTemplateFieldDialogOpen(true);
}}
>
<Plus className="h-4 w-4 mr-2" />
항목 추가
</Button>
</div>
{template.fields.length === 0 ? (
<div className="bg-gray-50 border-2 border-dashed border-gray-200 rounded-lg py-16">
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-lg flex items-center justify-center">
<FileText className="w-8 h-8 text-gray-400" />
</div>
<p className="text-gray-600 mb-1">
항목을 활용을 구간이에만 추가 버튼을 클릭해보세요
</p>
<p className="text-sm text-gray-500">
품목의 목록명, 수량, 입력방법 고객화된 표시할 수 있습니다
</p>
</div>
</div>
) : (
<div className="space-y-2">
{template.fields.map((field, _index) => (
<div
key={field.id}
className="flex items-center justify-between p-3 border rounded hover:bg-gray-50 transition-colors"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium">{field.name}</span>
<Badge variant="outline" className="text-xs">
{INPUT_TYPE_OPTIONS.find(t => t.value === field.property.inputType)?.label}
</Badge>
{field.property.required && (
<Badge variant="destructive" className="text-xs">필수</Badge>
)}
</div>
<div className="ml-6 text-xs text-gray-500 mt-1">
필드키: {field.fieldKey}
{field.description && (
<span className="ml-2">• {field.description}</span>
)}
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => handleEditTemplateField(template.id, field)}
>
<Edit className="h-4 w-4 text-blue-500" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteTemplateField(template.id, field.id)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
{/* 모듈 섹션 (BOM) 탭 */}
<TabsContent value="module">
{sectionTemplates.filter(t => t.type === 'bom').length === 0 ? (
<div className="text-center py-12">
<Package className="w-16 h-16 mx-auto text-gray-300 mb-4" />
<p className="text-muted-foreground mb-2">등록된 모듈 섹션이 없습니다</p>
<p className="text-sm text-muted-foreground">
섹션추가 버튼을 눌러 BOM 모듈 섹션을 등록하세요.
</p>
</div>
) : (
<div className="space-y-4">
{sectionTemplates.filter(t => t.type === 'bom').map((template) => (
<Card key={template.id}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<Package className="h-5 w-5 text-green-500" />
<div className="flex-1">
<CardTitle className="text-base">{template.title}</CardTitle>
{template.description && (
<CardDescription className="text-sm mt-0.5">{template.description}</CardDescription>
)}
</div>
</div>
<div className="flex items-center gap-2">
{template.category && template.category.length > 0 && (
<div className="flex flex-wrap gap-1 mr-2">
{template.category.map((cat, idx) => (
<Badge key={idx} variant="secondary" className="text-xs">
{ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat}
</Badge>
))}
</div>
)}
<Button
size="sm"
variant="ghost"
onClick={() => handleEditSectionTemplate(template)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteSectionTemplate(template.id)}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<BOMManagementSection
title=""
description=""
bomItems={template.bomItems || []}
onAddItem={(item) => handleAddBOMItemToTemplate(template.id, item)}
onUpdateItem={(itemId, item) => handleUpdateBOMItemInTemplate(template.id, itemId, item)}
onDeleteItem={(itemId) => handleDeleteBOMItemFromTemplate(template.id, itemId)}
/>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</TabsContent>
{/* 계층구조 탭 */}
<TabsContent value="hierarchy" className="space-y-4">
<HierarchyTab
itemPages={itemPages}
selectedPage={selectedPage}
ITEM_TYPE_OPTIONS={ITEM_TYPE_OPTIONS}
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={hasUnsavedChanges}
pendingChanges={pendingChanges}
selectedSectionForField={selectedSectionForField}
setSelectedSectionForField={setSelectedSectionForField}
newSectionType={newSectionType}
setNewSectionType={setNewSectionType}
updateItemPage={updateItemPage}
trackChange={trackChange}
deleteItemPage={deleteItemPage}
setIsPageDialogOpen={setIsPageDialogOpen}
setIsSectionDialogOpen={setIsSectionDialogOpen}
setIsFieldDialogOpen={setIsFieldDialogOpen}
handleEditSectionTitle={handleEditSectionTitle}
handleSaveSectionTitle={handleSaveSectionTitle}
moveSection={moveSection}
deleteSection={deleteSection}
updateSection={updateSection}
deleteField={deleteField}
handleEditField={handleEditField}
moveField={moveField}
/>
</TabsContent>
{/* 품목분류 탭 */}
<TabsContent value="categories">
<CategoryTab
itemCategories={itemCategories}
setItemCategories={setItemCategories}
newCategory1={newCategory1}
setNewCategory1={setNewCategory1}
newCategory2={newCategory2}
setNewCategory2={setNewCategory2}
newCategory3={newCategory3}
setNewCategory3={setNewCategory3}
selectedCategory1={selectedCategory1}
setSelectedCategory1={setSelectedCategory1}
selectedCategory2={selectedCategory2}
setSelectedCategory2={setSelectedCategory2}
/>
</TabsContent>
{/* 사용자 정의 탭들 (품목분류 제외) */}
{customTabs.filter(tab => !tab.isDefault && tab.id !== 'categories').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}
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}
/>
{/* 칼럼 관리 다이얼로그 */}
<Dialog open={isColumnManageDialogOpen} onOpenChange={setIsColumnManageDialogOpen}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>칼럼 관리</DialogTitle>
<DialogDescription>
{managingColumnType === 'units' && '단위'}
{managingColumnType === 'materials' && '재질'}
{managingColumnType === 'surface' && '표면처리'}
{managingColumnType && !['units', 'materials', 'surface'].includes(managingColumnType) &&
(attributeSubTabs.find(t => t.key === managingColumnType)?.label || '속성')}
{' '}에 추가 칼럼을 설정합니다 (예: 규격 안에 속성/값/단위 나누기)
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 기존 칼럼 목록 */}
{managingColumnType && attributeColumns[managingColumnType]?.length > 0 && (
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-3">설정된 칼럼</h4>
<div className="space-y-2">
{attributeColumns[managingColumnType].map((column, idx) => (
<div key={column.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div className="flex items-center gap-3">
<Badge variant="outline">{idx + 1}</Badge>
<div>
<p className="font-medium">{column.name}</p>
<p className="text-xs text-muted-foreground">
키: {column.key} | 타입: {column.type === 'text' ? '텍스트' : '숫자'}
{column.required && ' | 필수'}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (managingColumnType) {
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: prev[managingColumnType]?.filter(c => c.id !== column.id) || []
}));
}
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
))}
</div>
</div>
)}
{/* 새 칼럼 추가 폼 */}
<div className="border rounded-lg p-4 space-y-3">
<h4 className="font-medium">새 칼럼 추가</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>칼럼명 *</Label>
<Input
value={newColumnName}
onChange={(e) => setNewColumnName(e.target.value)}
placeholder="예: 속성, 값, 단위"
/>
</div>
<div>
<Label>키 (영문) *</Label>
<Input
value={newColumnKey}
onChange={(e) => setNewColumnKey(e.target.value)}
placeholder="예: property, value, unit"
/>
</div>
<div>
<Label>타입</Label>
<Select value={newColumnType} onValueChange={(value: 'text' | 'number') => setNewColumnType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text">텍스트</SelectItem>
<SelectItem value="number">숫자</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 pt-6">
<Switch
checked={newColumnRequired}
onCheckedChange={setNewColumnRequired}
/>
<Label>필수 항목</Label>
</div>
</div>
<Button
size="sm"
className="w-full"
onClick={() => {
if (!newColumnName.trim() || !newColumnKey.trim()) {
toast.error('칼럼명과 키를 입력해주세요');
return;
}
if (managingColumnType) {
const newColumn: OptionColumn = {
id: `col-${Date.now()}`,
name: newColumnName,
key: newColumnKey,
type: newColumnType,
required: newColumnRequired
};
setAttributeColumns(prev => ({
...prev,
[managingColumnType]: [...(prev[managingColumnType] || []), newColumn]
}));
// 입력 필드 초기화
setNewColumnName('');
setNewColumnKey('');
setNewColumnType('text');
setNewColumnRequired(false);
toast.success('칼럼이 추가되었습니다');
}
}}
>
<Plus className="w-4 h-4 mr-2" />
칼럼 추가
</Button>
</div>
</div>
<DialogFooter>
<Button onClick={() => setIsColumnManageDialogOpen(false)}>완료</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 절대경로 편집 다이얼로그 */}
<Dialog open={editingPathPageId !== null} onOpenChange={(open) => !open && setEditingPathPageId(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>절대경로 수정</DialogTitle>
<DialogDescription>페이지의 절대경로를 수정합니다 (예: /제품관리/제품등록)</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>절대경로 *</Label>
<Input
value={editingAbsolutePath}
onChange={(e) => setEditingAbsolutePath(e.target.value)}
placeholder="/제품관리/제품등록"
/>
<p className="text-xs text-gray-500 mt-1">슬래시(/)로 시작하며, 경로를 슬래시로 구분합니다</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingPathPageId(null)}>취소</Button>
<Button onClick={() => {
if (!editingAbsolutePath.trim()) {
toast.error('절대경로를 입력해주세요');
return;
}
if (!editingAbsolutePath.startsWith('/')) {
toast.error('절대경로는 슬래시(/)로 시작해야 합니다');
return;
}
if (editingPathPageId) {
updateItemPage(editingPathPageId, { absolutePath: editingAbsolutePath });
trackChange('pages', editingPathPageId, 'update', { absolutePath: editingAbsolutePath });
setEditingPathPageId(null);
toast.success('절대경로가 수정되었습니다 (저장 필요)');
}
}}>저장</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 섹션 추가 다이얼로그 */}
<Dialog open={isPageDialogOpen} onOpenChange={setIsPageDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>섹션 추가</DialogTitle>
<DialogDescription>새로운 품목 섹션을 생성합니다</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>섹션명 *</Label>
<Input
value={newPageName}
onChange={(e) => setNewPageName(e.target.value)}
placeholder="예: 품목 등록"
/>
</div>
<div>
<Label>품목유형 *</Label>
<Select value={newPageItemType} onValueChange={(v: any) => setNewPageItemType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ITEM_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsPageDialogOpen(false)}>취소</Button>
<Button onClick={handleAddPage}>추가</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 섹션 추가 다이얼로그 */}
<Dialog open={isSectionDialogOpen} onOpenChange={(open) => {
setIsSectionDialogOpen(open);
if (!open) setNewSectionType('fields');
}}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{newSectionType === 'bom' ? 'BOM 섹션' : '일반 섹션'} 추가</DialogTitle>
<DialogDescription>
{newSectionType === 'bom'
? '새로운 BOM(자재명세서) 섹션을 추가합니다'
: '새로운 일반 섹션을 추가합니다'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>섹션 제목 *</Label>
<Input
value={newSectionTitle}
onChange={(e) => setNewSectionTitle(e.target.value)}
placeholder={newSectionType === 'bom' ? '예: BOM 구성' : '예: 기본 정보'}
/>
</div>
<div>
<Label>설명 (선택)</Label>
<Textarea
value={newSectionDescription}
onChange={(e) => setNewSectionDescription(e.target.value)}
placeholder="섹션에 대한 설명"
/>
</div>
{newSectionType === 'bom' && (
<div className="bg-blue-50 p-3 rounded-md">
<p className="text-sm text-blue-700">
<strong>BOM 섹션:</strong> 자재명세서(BOM) 관리를 위한 전용 섹션입니다.
부품 구성, 수량, 단가 등을 관리할 수 있습니다.
</p>
</div>
)}
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button variant="outline" onClick={() => {
setIsSectionDialogOpen(false);
setNewSectionType('fields');
}} className="w-full sm:w-auto">취소</Button>
<Button onClick={handleAddSection} className="w-full sm:w-auto">추가</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 항목 추가/수정 다이얼로그 - 데스크톱 */}
{!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}
itemMasterFields={itemMasterFields}
handleAddField={handleAddField}
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}
itemMasterFields={itemMasterFields}
handleAddField={handleAddField}
setIsColumnDialogOpen={setIsColumnDialogOpen}
setEditingColumnId={setEditingColumnId}
setColumnName={setColumnName}
setColumnKey={setColumnKey}
/>
)}
{/* 텍스트박스 컬럼 추가/수정 다이얼로그 */}
<Dialog open={isColumnDialogOpen} onOpenChange={setIsColumnDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingColumnId ? '컬럼 수정' : '컬럼 추가'}</DialogTitle>
<DialogDescription>
텍스트박스에 추가할 컬럼 정보를 입력하세요
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>컬럼명 *</Label>
<Input
value={columnName}
onChange={(e) => setColumnName(e.target.value)}
placeholder="예: 가로"
/>
</div>
<div>
<Label>컬럼 키 *</Label>
<Input
value={columnKey}
onChange={(e) => setColumnKey(e.target.value)}
placeholder="예: width"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsColumnDialogOpen(false)}>취소</Button>
<Button onClick={() => {
if (!columnName.trim() || !columnKey.trim()) {
return toast.error('모든 필드를 입력해주세요');
}
if (editingColumnId) {
// 수정
setTextboxColumns(prev => prev.map(col =>
col.id === editingColumnId
? { ...col, name: columnName, key: columnKey }
: col
));
toast.success('컬럼이 수정되었습니다');
} else {
// 추가
setTextboxColumns(prev => [...prev, {
id: `col-${Date.now()}`,
name: columnName,
key: columnKey
}]);
toast.success('컬럼이 추가되었습니다');
}
setIsColumnDialogOpen(false);
setEditingColumnId(null);
setColumnName('');
setColumnKey('');
}}>
{editingColumnId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 마스터 항목 추가/수정 다이얼로그 */}
<Dialog open={isMasterFieldDialogOpen} onOpenChange={(open) => {
setIsMasterFieldDialogOpen(open);
if (!open) {
setEditingMasterFieldId(null);
setNewMasterFieldName('');
setNewMasterFieldKey('');
setNewMasterFieldInputType('textbox');
setNewMasterFieldRequired(false);
setNewMasterFieldCategory('공통');
setNewMasterFieldDescription('');
setNewMasterFieldOptions('');
setNewMasterFieldAttributeType('custom');
setNewMasterFieldMultiColumn(false);
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingMasterFieldId ? '마스터 항목 수정' : '마스터 항목 추가'}</DialogTitle>
<DialogDescription>
여러 섹션에서 재사용할 수 있는 항목 템플릿을 생성합니다
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label>항목명 *</Label>
<Input
value={newMasterFieldName}
onChange={(e) => setNewMasterFieldName(e.target.value)}
placeholder="예: 품목명"
/>
</div>
<div>
<Label>필드 키 *</Label>
<Input
value={newMasterFieldKey}
onChange={(e) => setNewMasterFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>입력방식 *</Label>
<Select value={newMasterFieldInputType} onValueChange={(v: any) => setNewMasterFieldInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INPUT_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Switch checked={newMasterFieldRequired} onCheckedChange={setNewMasterFieldRequired} />
<Label>필수 항목</Label>
</div>
<div>
<Label>설명</Label>
<Textarea
value={newMasterFieldDescription}
onChange={(e) => setNewMasterFieldDescription(e.target.value)}
placeholder="항목에 대한 설명"
/>
<p className="text-xs text-gray-500 mt-1">* 제품 유형에 따라 품목 분류를 표시 [필수]</p>
</div>
{(newMasterFieldInputType === 'textbox' || newMasterFieldInputType === 'textarea') && (
<div className="space-y-4 p-4 border rounded-lg bg-gray-50">
<div className="flex items-center gap-2">
<Switch
checked={newMasterFieldMultiColumn}
onCheckedChange={(checked) => {
setNewMasterFieldMultiColumn(checked);
if (!checked) {
setNewMasterFieldColumnCount(2);
setNewMasterFieldColumnNames(['컬럼1', '컬럼2']);
}
}}
/>
<Label>다중 컬럼 사용</Label>
</div>
<p className="text-xs text-gray-500">
활성화하면 한 항목에 여러 개의 값을 입력받을 수 있습니다 (예: 규격 - 가로, 세로, 높이)
</p>
{newMasterFieldMultiColumn && (
<div className="space-y-4 pt-4 border-t">
<div>
<Label>컬럼 개수</Label>
<Input
type="number"
min="2"
max="10"
value={newMasterFieldColumnCount}
onChange={(e) => {
const count = parseInt(e.target.value) || 2;
setNewMasterFieldColumnCount(count);
// 컬럼 개수에 맞게 이름 배열 조정
const newNames = Array.from({ length: count }, (_, i) =>
newMasterFieldColumnNames[i] || `컬럼${i + 1}`
);
setNewMasterFieldColumnNames(newNames);
}}
placeholder="컬럼 개수 (2~10)"
/>
</div>
<div>
<Label>컬럼 이름 설정</Label>
<div className="space-y-2 mt-2">
{Array.from({ length: newMasterFieldColumnCount }, (_, i) => (
<Input
key={i}
value={newMasterFieldColumnNames[i] || ''}
onChange={(e) => {
const newNames = [...newMasterFieldColumnNames];
newNames[i] = e.target.value;
setNewMasterFieldColumnNames(newNames);
}}
placeholder={`${i + 1}번째 컬럼 이름`}
/>
))}
</div>
<p className="text-xs text-gray-500 mt-2">
예시: 가로, 세로, 높이 / 최소값, 최대값 / 상한, 하한
</p>
</div>
</div>
)}
</div>
)}
{newMasterFieldInputType === 'dropdown' && (
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<Label>드롭다운 옵션</Label>
{newMasterFieldAttributeType !== 'custom' && (
<Badge variant="secondary" className="text-xs">
{newMasterFieldAttributeType === 'unit' ? '단위' :
newMasterFieldAttributeType === 'material' ? '재질' : '표면처리'} 연동
</Badge>
)}
</div>
<Textarea
value={newMasterFieldOptions}
onChange={(e) => setNewMasterFieldOptions(e.target.value)}
placeholder="제품,부품,원자재 (쉼표로 구분)"
disabled={newMasterFieldAttributeType !== 'custom'}
className="min-h-[80px]"
/>
<p className="text-xs text-gray-500 mt-1">
{newMasterFieldAttributeType === 'custom'
? '쉼표(,)로 구분하여 입력하세요'
: '속성 탭에서 옵션을 추가/삭제하면 자동으로 반영됩니다'
}
</p>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsMasterFieldDialogOpen(false)}>취소</Button>
<Button onClick={editingMasterFieldId ? handleUpdateMasterField : handleAddMasterField}>
{editingMasterFieldId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 섹션 추가/수정 다이얼로그 */}
<Dialog open={isSectionTemplateDialogOpen} onOpenChange={(open) => {
setIsSectionTemplateDialogOpen(open);
if (!open) {
setEditingSectionTemplateId(null);
setNewSectionTemplateTitle('');
setNewSectionTemplateDescription('');
setNewSectionTemplateCategory([]);
setNewSectionTemplateType('fields');
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingSectionTemplateId ? '섹션 수정' : '섹션 추가'}</DialogTitle>
<DialogDescription>
여러 페이지에서 재사용할 수 있는 섹션을 생성합니다
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>섹션 제목 *</Label>
<Input
value={newSectionTemplateTitle}
onChange={(e) => setNewSectionTemplateTitle(e.target.value)}
placeholder="예: 기본 정보"
/>
</div>
<div>
<Label>설명 (선택)</Label>
<Textarea
value={newSectionTemplateDescription}
onChange={(e) => setNewSectionTemplateDescription(e.target.value)}
placeholder="섹션에 대한 설명"
/>
</div>
<div>
<Label>섹션 타입 *</Label>
<Select
value={newSectionTemplateType}
onValueChange={(val) => setNewSectionTemplateType(val as 'fields' | 'bom')}
disabled={!!editingSectionTemplateId}
>
<SelectTrigger>
<SelectValue placeholder="섹션 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fields">일반 필드</SelectItem>
<SelectItem value="bom">BOM (부품 구성)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500 mt-1">
{editingSectionTemplateId
? '※ 템플릿 타입은 수정할 수 없습니다.'
: '일반 필드: 텍스트, 드롭다운 등의 항목 관리 | BOM: 하위 품목 구성 관리'}
</p>
</div>
<div>
<Label>적용 카테고리 (선택)</Label>
<div className="grid grid-cols-3 gap-2 mt-2">
{ITEM_TYPE_OPTIONS.map((type) => (
<div key={type.value} className="flex items-center gap-2">
<input
type="checkbox"
id={`cat-${type.value}`}
checked={newSectionTemplateCategory.includes(type.value)}
onChange={(e) => {
if (e.target.checked) {
setNewSectionTemplateCategory([...newSectionTemplateCategory, type.value]);
} else {
setNewSectionTemplateCategory(newSectionTemplateCategory.filter(c => c !== type.value));
}
}}
className="rounded"
/>
<label htmlFor={`cat-${type.value}`} className="text-sm cursor-pointer">
{type.label}
</label>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-2">
선택하지 않으면 모든 카테고리에서 사용 가능합니다
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsSectionTemplateDialogOpen(false)}>취소</Button>
<Button onClick={editingSectionTemplateId ? handleUpdateSectionTemplate : handleAddSectionTemplate}>
{editingSectionTemplateId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 섹션 템플릿 항목 추가/수정 다이얼로그 */}
<Dialog open={isTemplateFieldDialogOpen} onOpenChange={(open) => {
setIsTemplateFieldDialogOpen(open);
if (!open) {
setEditingTemplateFieldId(null);
setTemplateFieldName('');
setTemplateFieldKey('');
setTemplateFieldInputType('textbox');
setTemplateFieldRequired(false);
setTemplateFieldOptions('');
setTemplateFieldDescription('');
setTemplateFieldMultiColumn(false);
setTemplateFieldColumnCount(2);
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
}
}}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingTemplateFieldId ? '항목 수정' : '항목 추가'}</DialogTitle>
<DialogDescription>
섹션에 포함될 항목을 설정합니다
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label>항목명 *</Label>
<Input
value={templateFieldName}
onChange={(e) => setTemplateFieldName(e.target.value)}
placeholder="예: 품목명"
/>
</div>
<div>
<Label>필드 키 *</Label>
<Input
value={templateFieldKey}
onChange={(e) => setTemplateFieldKey(e.target.value)}
placeholder="예: itemName"
/>
</div>
</div>
<div>
<Label>입력방식 *</Label>
<Select value={templateFieldInputType} onValueChange={(v: any) => setTemplateFieldInputType(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INPUT_TYPE_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{templateFieldInputType === 'dropdown' && (
<div>
<Label>드롭다운 옵션</Label>
<Input
value={templateFieldOptions}
onChange={(e) => setTemplateFieldOptions(e.target.value)}
placeholder="옵션1, 옵션2, 옵션3 (쉼표로 구분)"
/>
</div>
)}
{(templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && (
<div className="space-y-3 border rounded-lg p-4 bg-muted/30">
<div className="flex items-center gap-2">
<Switch
checked={templateFieldMultiColumn}
onCheckedChange={setTemplateFieldMultiColumn}
/>
<Label>다중 컬럼 사용</Label>
</div>
{templateFieldMultiColumn && (
<>
<div>
<Label>컬럼 개수</Label>
<Input
type="number"
min={2}
max={10}
value={templateFieldColumnCount}
onChange={(e) => {
const count = parseInt(e.target.value) || 2;
setTemplateFieldColumnCount(count);
const newNames = Array.from({ length: count }, (_, i) =>
templateFieldColumnNames[i] || `컬럼${i + 1}`
);
setTemplateFieldColumnNames(newNames);
}}
/>
</div>
<div className="space-y-2">
<Label>컬럼명</Label>
{Array.from({ length: templateFieldColumnCount }).map((_, idx) => (
<Input
key={idx}
placeholder={`컬럼 ${idx + 1}`}
value={templateFieldColumnNames[idx] || ''}
onChange={(e) => {
const newNames = [...templateFieldColumnNames];
newNames[idx] = e.target.value;
setTemplateFieldColumnNames(newNames);
}}
/>
))}
</div>
</>
)}
</div>
)}
<div>
<Label>설명 (선택)</Label>
<Textarea
value={templateFieldDescription}
onChange={(e) => setTemplateFieldDescription(e.target.value)}
placeholder="항목에 대한 설명"
/>
</div>
<div className="flex items-center gap-2">
<Switch checked={templateFieldRequired} onCheckedChange={setTemplateFieldRequired} />
<Label>필수 항목</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsTemplateFieldDialogOpen(false)}>취소</Button>
<Button onClick={handleAddTemplateField}>
{editingTemplateFieldId ? '수정' : '추가'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 섹션 템플릿 불러오기 다이얼로그 */}
<Dialog open={isLoadTemplateDialogOpen} onOpenChange={(open) => {
setIsLoadTemplateDialogOpen(open);
if (!open) {
setSelectedTemplateId(null);
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>섹션 불러오기</DialogTitle>
<DialogDescription>
저장된 섹션을 선택하여 현재 페이지에 추가합니다
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{sectionTemplates.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">등록된 섹션이 없습니다</p>
<p className="text-sm text-muted-foreground mt-1">
먼저 "섹션관리" 탭에서 섹션을 생성해주세요
</p>
</div>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{sectionTemplates.map((template) => (
<div
key={template.id}
onClick={() => setSelectedTemplateId(template.id)}
className={`p-4 border rounded-lg cursor-pointer transition-all ${
selectedTemplateId === template.id
? 'border-primary bg-primary/5 shadow-md'
: 'border-border hover:border-primary/50 hover:bg-accent'
}`}
>
<div className="flex items-start gap-3">
{template.type === 'bom' ? (
<Package className={`h-5 w-5 mt-0.5 ${
selectedTemplateId === template.id ? 'text-primary' : 'text-muted-foreground'
}`} />
) : (
<Folder className={`h-5 w-5 mt-0.5 ${
selectedTemplateId === template.id ? 'text-primary' : 'text-muted-foreground'
}`} />
)}
<div className="flex-1">
<div className="font-semibold mb-1">{template.title}</div>
{template.description && (
<p className="text-sm text-muted-foreground mb-2">{template.description}</p>
)}
<div className="flex items-center gap-2 flex-wrap">
{template.type === 'bom' ? (
<Badge variant="secondary" className="text-xs">
BOM {(template.bomItems || []).length}개
</Badge>
) : (
<Badge variant="secondary" className="text-xs">
항목 {template.fields.length}개
</Badge>
)}
{template.category && template.category.length > 0 && template.category.map((cat, idx) => (
<Badge key={idx} variant="outline" className="text-xs">
{ITEM_TYPE_OPTIONS.find(t => t.value === cat)?.label || cat}
</Badge>
))}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsLoadTemplateDialogOpen(false)}>취소</Button>
<Button
onClick={handleLoadTemplate}
disabled={!selectedTemplateId}
>
불러오기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</PageLayout>
);
}