- 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>
3926 lines
174 KiB
Plaintext
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>
|
|
);
|
|
}
|