2025-11-18 14:17:52 +09:00
|
|
|
'use client';
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
import { useState, useEffect } from 'react';
|
2025-11-18 14:17:52 +09:00
|
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
|
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
2025-11-23 16:10:27 +09:00
|
|
|
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
2025-11-25 21:07:10 +09:00
|
|
|
import type { ItemPage, ItemSection, ItemField, FieldDisplayCondition, ItemMasterField, ItemFieldProperty, SectionTemplate, BOMItem, TemplateField } from '@/contexts/ItemMasterContext';
|
2025-11-23 16:10:27 +09:00
|
|
|
import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs';
|
|
|
|
|
import { FieldDialog } from './ItemMasterDataManagement/dialogs/FieldDialog';
|
|
|
|
|
import { type ConditionalFieldConfig } from './ItemMasterDataManagement/components/ConditionalDisplayUI';
|
|
|
|
|
import { FieldDrawer } from './ItemMasterDataManagement/dialogs/FieldDrawer';
|
|
|
|
|
import { TabManagementDialogs } from './ItemMasterDataManagement/dialogs/TabManagementDialogs';
|
|
|
|
|
import { OptionDialog } from './ItemMasterDataManagement/dialogs/OptionDialog';
|
|
|
|
|
import { ColumnManageDialog } from './ItemMasterDataManagement/dialogs/ColumnManageDialog';
|
|
|
|
|
import { PathEditDialog } from './ItemMasterDataManagement/dialogs/PathEditDialog';
|
|
|
|
|
import { PageDialog } from './ItemMasterDataManagement/dialogs/PageDialog';
|
|
|
|
|
import { SectionDialog } from './ItemMasterDataManagement/dialogs/SectionDialog';
|
|
|
|
|
import { MasterFieldDialog } from './ItemMasterDataManagement/dialogs/MasterFieldDialog';
|
|
|
|
|
import { TemplateFieldDialog } from './ItemMasterDataManagement/dialogs/TemplateFieldDialog';
|
|
|
|
|
import { LoadTemplateDialog } from './ItemMasterDataManagement/dialogs/LoadTemplateDialog';
|
|
|
|
|
import { ColumnDialog } from './ItemMasterDataManagement/dialogs/ColumnDialog';
|
|
|
|
|
import { SectionTemplateDialog } from './ItemMasterDataManagement/dialogs/SectionTemplateDialog';
|
|
|
|
|
import { itemMasterApi } from '@/lib/api/item-master';
|
|
|
|
|
import { getErrorMessage, ApiError } from '@/lib/api/error-handler';
|
|
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
|
|
|
|
import { ErrorMessage } from '@/components/ui/error-message';
|
|
|
|
|
import {
|
|
|
|
|
transformPageResponse,
|
|
|
|
|
transformPagesResponse,
|
|
|
|
|
transformSectionTemplatesResponse,
|
|
|
|
|
transformMasterFieldsResponse,
|
|
|
|
|
transformCustomTabsResponse,
|
|
|
|
|
transformUnitOptionsResponse,
|
|
|
|
|
} from '@/lib/api/transformers';
|
|
|
|
|
import {
|
|
|
|
|
Database,
|
|
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
2025-11-18 14:17:52 +09:00
|
|
|
FolderTree,
|
|
|
|
|
Folder,
|
|
|
|
|
FileText,
|
|
|
|
|
Settings,
|
|
|
|
|
ListTree,
|
|
|
|
|
Package,
|
2025-11-23 16:10:27 +09:00
|
|
|
Layers
|
2025-11-18 14:17:52 +09:00
|
|
|
} from 'lucide-react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 로컬 타입 import
|
|
|
|
|
import type { OptionColumn, MasterOption } from './ItemMasterDataManagement/types';
|
|
|
|
|
// Utils import
|
|
|
|
|
import { generateAbsolutePath } from './ItemMasterDataManagement/utils/pathUtils';
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// 초기 데이터
|
2025-11-23 16:10:27 +09:00
|
|
|
const INITIAL_UNIT_OPTIONS: MasterOption[] = [];
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const INITIAL_MATERIAL_OPTIONS: MasterOption[] = [];
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const INITIAL_SURFACE_TREATMENT_OPTIONS: MasterOption[] = [];
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
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,
|
2025-11-25 21:07:10 +09:00
|
|
|
loadItemPages,
|
2025-11-18 14:17:52 +09:00
|
|
|
addItemPage,
|
|
|
|
|
updateItemPage,
|
|
|
|
|
deleteItemPage,
|
|
|
|
|
addSectionToPage,
|
|
|
|
|
updateSection,
|
|
|
|
|
deleteSection,
|
|
|
|
|
addFieldToSection,
|
|
|
|
|
updateField,
|
|
|
|
|
deleteField,
|
|
|
|
|
reorderFields,
|
|
|
|
|
itemMasterFields,
|
2025-11-25 21:07:10 +09:00
|
|
|
loadItemMasterFields,
|
2025-11-18 14:17:52 +09:00
|
|
|
addItemMasterField,
|
|
|
|
|
updateItemMasterField,
|
|
|
|
|
deleteItemMasterField,
|
|
|
|
|
sectionTemplates,
|
2025-11-25 21:07:10 +09:00
|
|
|
loadSectionTemplates,
|
2025-11-18 14:17:52 +09:00
|
|
|
addSectionTemplate,
|
|
|
|
|
updateSectionTemplate,
|
2025-11-23 16:10:27 +09:00
|
|
|
deleteSectionTemplate,
|
2025-11-25 21:07:10 +09:00
|
|
|
resetAllData,
|
|
|
|
|
tenantId
|
2025-11-23 16:10:27 +09:00
|
|
|
} = useItemMaster();
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
console.log('ItemMasterDataManagement: Current sectionTemplates', sectionTemplates);
|
|
|
|
|
|
|
|
|
|
// 모든 페이지의 섹션을 하나의 배열로 평탄화
|
2025-11-23 16:10:27 +09:00
|
|
|
const _itemSections = itemPages.flatMap(page =>
|
2025-11-18 14:17:52 +09:00
|
|
|
page.sections.map(section => ({
|
|
|
|
|
...section,
|
|
|
|
|
parentPageId: page.id
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 마운트 상태 추적 (SSR 호환)
|
|
|
|
|
const [_mounted, setMounted] = useState(false);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
setMounted(true);
|
|
|
|
|
}, []);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// API 로딩 및 에러 상태 관리
|
|
|
|
|
const [isInitialLoading, setIsInitialLoading] = useState(true); // 초기 데이터 로딩
|
|
|
|
|
const [_isLoading, setIsLoading] = useState(false); // 개별 작업 로딩
|
|
|
|
|
const [error, setError] = useState<string | null>(null); // 에러 메시지
|
|
|
|
|
|
|
|
|
|
// 초기 데이터 로딩
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadInitialData = async () => {
|
2025-11-18 14:17:52 +09:00
|
|
|
try {
|
2025-11-23 16:10:27 +09:00
|
|
|
setIsInitialLoading(true);
|
|
|
|
|
setError(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const data = await itemMasterApi.init();
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 페이지 데이터 로드 (이미 존재하는 데이터를 state에 로드 - API 호출 없음)
|
|
|
|
|
const transformedPages = transformPagesResponse(data.pages);
|
|
|
|
|
loadItemPages(transformedPages);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 섹션 템플릿 로드 (덮어쓰기 - API 호출 없음!)
|
|
|
|
|
const transformedTemplates = transformSectionTemplatesResponse(data.sectionTemplates);
|
|
|
|
|
loadSectionTemplates(transformedTemplates);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 마스터 필드 로드 (덮어쓰기 - API 호출 없음!)
|
|
|
|
|
const transformedFields = transformMasterFieldsResponse(data.masterFields);
|
|
|
|
|
loadItemMasterFields(transformedFields);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 커스텀 탭 로드 (local state)
|
|
|
|
|
if (data.customTabs && data.customTabs.length > 0) {
|
|
|
|
|
const transformedTabs = transformCustomTabsResponse(data.customTabs);
|
|
|
|
|
setCustomTabs(prev => [...prev, ...transformedTabs]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 단위 옵션 로드 (local state)
|
|
|
|
|
if (data.unitOptions && data.unitOptions.length > 0) {
|
|
|
|
|
const transformedUnits = transformUnitOptionsResponse(data.unitOptions);
|
|
|
|
|
setUnitOptions(transformedUnits);
|
|
|
|
|
}
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
console.log('✅ Initial data loaded:', {
|
|
|
|
|
pages: data.pages.length,
|
|
|
|
|
templates: data.sectionTemplates.length,
|
|
|
|
|
masterFields: data.masterFields.length,
|
|
|
|
|
customTabs: data.customTabs?.length || 0,
|
|
|
|
|
unitOptions: data.unitOptions?.length || 0,
|
|
|
|
|
});
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof ApiError && err.errors) {
|
|
|
|
|
// Validation 에러 (422)
|
|
|
|
|
const errorMessages = Object.entries(err.errors)
|
|
|
|
|
.map(([field, messages]) => `${field}: ${messages.join(', ')}`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
toast.error(errorMessages);
|
|
|
|
|
setError('입력값을 확인해주세요.');
|
|
|
|
|
} else {
|
|
|
|
|
const errorMessage = getErrorMessage(err);
|
|
|
|
|
setError(errorMessage);
|
|
|
|
|
toast.error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
console.error('❌ Failed to load initial data:', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsInitialLoading(false);
|
2025-11-18 14:17:52 +09:00
|
|
|
}
|
2025-11-23 16:10:27 +09:00
|
|
|
};
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
loadInitialData();
|
|
|
|
|
}, []);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 동적 탭 관리 - 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 }
|
|
|
|
|
];
|
2025-11-18 14:17:52 +09:00
|
|
|
});
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState('hierarchy');
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 속성 하위 탭 관리 (API에서 로드)
|
|
|
|
|
const [attributeSubTabs, setAttributeSubTabs] = useState<Array<{id: string; label: string; key: string; isDefault: boolean; order: number}>>([]);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// 마스터 항목이 추가/수정될 때 속성 탭 자동 생성
|
|
|
|
|
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 => {
|
|
|
|
|
// 이미 탭이 있는지 확인
|
2025-11-23 16:10:27 +09:00
|
|
|
const existingTab = attributeSubTabs.find(tab => tab.key === field.id.toString());
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
if (!existingTab) {
|
|
|
|
|
// 새로운 탭 추가 대상
|
|
|
|
|
const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), ...newTabs.map(t => t.order), -1);
|
|
|
|
|
const newTab = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: `attr-${field.id.toString()}`,
|
|
|
|
|
label: field.field_name,
|
|
|
|
|
key: field.id.toString(),
|
2025-11-18 14:17:52 +09:00
|
|
|
isDefault: false,
|
|
|
|
|
order: maxOrder + 1
|
|
|
|
|
};
|
|
|
|
|
newTabs.push(newTab);
|
2025-11-23 16:10:27 +09:00
|
|
|
} else if (existingTab.label !== field.field_name) {
|
2025-11-18 14:17:52 +09:00
|
|
|
// 이름이 변경된 경우
|
2025-11-23 16:10:27 +09:00
|
|
|
updatedTabs.push({ ...existingTab, label: field.field_name });
|
2025-11-18 14:17:52 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 상태 업데이트는 한 번만 수행
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const [unitOptions, setUnitOptions] = useState<MasterOption[]>(INITIAL_UNIT_OPTIONS);
|
|
|
|
|
const [materialOptions, setMaterialOptions] = useState<MasterOption[]>(INITIAL_MATERIAL_OPTIONS);
|
|
|
|
|
const [surfaceTreatmentOptions, setSurfaceTreatmentOptions] = useState<MasterOption[]>(INITIAL_SURFACE_TREATMENT_OPTIONS);
|
|
|
|
|
const [customAttributeOptions, setCustomAttributeOptions] = useState<Record<string, MasterOption[]>>({});
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
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);
|
2025-11-23 16:10:27 +09:00
|
|
|
const [attributeColumns, setAttributeColumns] = useState<Record<string, OptionColumn[]>>({});
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 칼럼 추가 폼 상태
|
|
|
|
|
const [newColumnName, setNewColumnName] = useState('');
|
|
|
|
|
const [newColumnKey, setNewColumnKey] = useState('');
|
|
|
|
|
const [newColumnType, setNewColumnType] = useState<'text' | 'number'>('text');
|
|
|
|
|
const [newColumnRequired, setNewColumnRequired] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 계층구조 상태
|
2025-11-23 16:10:27 +09:00
|
|
|
const [selectedPageId, setSelectedPageId] = useState<number | null>(itemPages[0]?.id || null);
|
|
|
|
|
const selectedPage = itemPages.find(p => p.id === selectedPageId);
|
|
|
|
|
const [_expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
2025-11-18 14:17:52 +09:00
|
|
|
const [editingSectionId, setEditingSectionId] = useState<string | null>(null);
|
|
|
|
|
const [editingSectionTitle, setEditingSectionTitle] = useState('');
|
|
|
|
|
|
|
|
|
|
// 기존 페이지들에 절대경로 자동 생성 (마이그레이션)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let needsUpdate = false;
|
|
|
|
|
itemPages.forEach(page => {
|
2025-11-23 16:10:27 +09:00
|
|
|
if (!page.absolute_path) {
|
|
|
|
|
const absolutePath = generateAbsolutePath(page.item_type, page.page_name);
|
|
|
|
|
updateItemPage(page.id, { absolute_path: absolutePath });
|
2025-11-18 14:17:52 +09:00
|
|
|
needsUpdate = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (needsUpdate) {
|
|
|
|
|
console.log('절대경로가 자동으로 생성되었습니다');
|
|
|
|
|
}
|
|
|
|
|
}, []); // 빈 의존성 배열로 최초 1회만 실행
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const [editingPageId, setEditingPageId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
const [editingPageName, setEditingPageName] = useState('');
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const [isPageDialogOpen, setIsPageDialogOpen] = useState(false);
|
|
|
|
|
const [newPageName, setNewPageName] = useState('');
|
|
|
|
|
const [newPageItemType, setNewPageItemType] = useState<'FG' | 'PT' | 'SM' | 'RM' | 'CS'>('FG');
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const [editingPathPageId, setEditingPathPageId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
const [editingAbsolutePath, setEditingAbsolutePath] = useState('');
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const [isSectionDialogOpen, setIsSectionDialogOpen] = useState(false);
|
|
|
|
|
const [newSectionTitle, setNewSectionTitle] = useState('');
|
|
|
|
|
const [newSectionDescription, setNewSectionDescription] = useState('');
|
|
|
|
|
const [newSectionType, setNewSectionType] = useState<'fields' | 'bom'>('fields');
|
2025-11-25 21:07:10 +09:00
|
|
|
const [sectionInputMode, setSectionInputMode] = useState<'custom' | 'template'>('custom');
|
|
|
|
|
const [selectedSectionTemplateId, setSelectedSectionTemplateId] = useState<number | null>(null);
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 모바일 체크
|
|
|
|
|
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);
|
2025-11-23 16:10:27 +09:00
|
|
|
const [selectedSectionForField, setSelectedSectionForField] = useState<number | null>(null);
|
|
|
|
|
const [editingFieldId, setEditingFieldId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
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');
|
2025-11-23 16:10:27 +09:00
|
|
|
const [newFieldConditionFields, setNewFieldConditionFields] = useState<ConditionalFieldConfig[]>([]);
|
2025-11-18 14:17:52 +09:00
|
|
|
const [newFieldConditionSections, setNewFieldConditionSections] = useState<string[]>([]);
|
|
|
|
|
// 임시 입력용
|
2025-11-23 16:10:27 +09:00
|
|
|
const [_tempConditionFieldKey, setTempConditionFieldKey] = useState('');
|
2025-11-18 14:17:52 +09:00
|
|
|
const [tempConditionValue, setTempConditionValue] = useState('');
|
|
|
|
|
|
|
|
|
|
// 마스터 항목 관리 상태
|
|
|
|
|
const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false);
|
2025-11-23 16:10:27 +09:00
|
|
|
const [editingMasterFieldId, setEditingMasterFieldId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
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);
|
2025-11-23 16:10:27 +09:00
|
|
|
const [editingSectionTemplateId, setEditingSectionTemplateId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// 섹션 템플릿 확장 상태
|
2025-11-23 16:10:27 +09:00
|
|
|
const [_expandedTemplateId, _setExpandedTemplateId] = useState<string | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// 섹션 템플릿 항목 추가 다이얼로그
|
|
|
|
|
const [isTemplateFieldDialogOpen, setIsTemplateFieldDialogOpen] = useState(false);
|
2025-11-23 16:10:27 +09:00
|
|
|
const [currentTemplateId, setCurrentTemplateId] = useState<number | null>(null);
|
|
|
|
|
const [editingTemplateFieldId, setEditingTemplateFieldId] = useState<number | null>(null);
|
2025-11-18 14:17:52 +09:00
|
|
|
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']);
|
2025-11-25 21:07:10 +09:00
|
|
|
// 템플릿 필드 마스터 항목 관련 상태
|
|
|
|
|
const [templateFieldInputMode, setTemplateFieldInputMode] = useState<'custom' | 'master'>('custom');
|
|
|
|
|
const [templateFieldShowMasterFieldList, setTemplateFieldShowMasterFieldList] = useState(false);
|
|
|
|
|
const [templateFieldSelectedMasterFieldId, setTemplateFieldSelectedMasterFieldId] = useState('');
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// BOM 관리 상태
|
2025-11-23 16:10:27 +09:00
|
|
|
const [_bomItems, setBomItems] = useState<BOMItem[]>([]);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// 속성 변경 시 연동된 마스터 항목의 옵션 자동 업데이트
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
itemMasterFields.forEach(field => {
|
2025-11-25 21:07:10 +09:00
|
|
|
// default_properties가 null/undefined인 경우 스킵
|
|
|
|
|
if (!field.default_properties) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
const attributeType = (field.default_properties as any).attributeType;
|
|
|
|
|
if (attributeType && attributeType !== 'custom' && field.default_properties?.inputType === 'dropdown') {
|
2025-11-18 14:17:52 +09:00
|
|
|
let newOptions: string[] = [];
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
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);
|
|
|
|
|
}
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
const currentOptions = field.default_properties?.options || [];
|
2025-11-18 14:17:52 +09:00
|
|
|
const optionsChanged = JSON.stringify(currentOptions.sort()) !== JSON.stringify(newOptions.sort());
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (optionsChanged && newOptions.length > 0) {
|
|
|
|
|
updateItemMasterField(field.id, {
|
|
|
|
|
...field,
|
2025-11-23 16:10:27 +09:00
|
|
|
default_properties: {
|
|
|
|
|
...(field.default_properties || {}),
|
2025-11-18 14:17:52 +09:00
|
|
|
options: newOptions
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, [unitOptions, materialOptions, surfaceTreatmentOptions, customAttributeOptions, itemMasterFields]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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('삭제되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 절대경로 자동 생성 함수
|
|
|
|
|
// 계층구조 핸들러
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleAddPage = async () => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!newPageName.trim()) return toast.error('섹션명을 입력해주세요');
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// Context의 addItemPage 사용 (API 호출 + state 업데이트)
|
|
|
|
|
// ⚠️ 이전 코드는 여기서 API 호출 후 addItemPage도 호출해서 API가 2번 호출되는 버그가 있었음
|
|
|
|
|
const newPage = await addItemPage({
|
2025-11-23 16:10:27 +09:00
|
|
|
page_name: newPageName,
|
|
|
|
|
item_type: newPageItemType,
|
|
|
|
|
absolute_path: absolutePath,
|
|
|
|
|
is_active: true,
|
2025-11-25 21:07:10 +09:00
|
|
|
sections: [],
|
|
|
|
|
order_no: 0,
|
2025-11-23 16:10:27 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 새로 생성된 페이지를 선택
|
2025-11-25 21:07:10 +09:00
|
|
|
setSelectedPageId(newPage.id);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
// 폼 초기화
|
|
|
|
|
setNewPageName('');
|
|
|
|
|
setNewPageItemType('FG');
|
|
|
|
|
setIsPageDialogOpen(false);
|
|
|
|
|
|
|
|
|
|
toast.success('페이지가 추가되었습니다');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof ApiError && err.errors) {
|
|
|
|
|
// Validation 에러 (422)
|
|
|
|
|
const errorMessages = Object.entries(err.errors)
|
|
|
|
|
.map(([field, messages]) => `${field}: ${messages.join(', ')}`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
toast.error(errorMessages);
|
|
|
|
|
} else {
|
|
|
|
|
const errorMessage = getErrorMessage(err);
|
|
|
|
|
toast.error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
console.error('❌ Failed to create page:', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
const handleDuplicatePage = async (pageId: number) => {
|
2025-11-23 16:10:27 +09:00
|
|
|
const originalPage = itemPages.find(p => p.id === pageId);
|
|
|
|
|
if (!originalPage) return toast.error('페이지를 찾을 수 없습니다');
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
try {
|
|
|
|
|
setIsLoading(true);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 페이지 복제
|
|
|
|
|
const duplicatedPageName = `${originalPage.page_name} (복제)`;
|
|
|
|
|
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
|
|
|
|
|
|
|
|
|
|
// Context의 addItemPage 사용 (API 호출 + state 업데이트)
|
|
|
|
|
const newPage = await addItemPage({
|
|
|
|
|
page_name: duplicatedPageName,
|
|
|
|
|
item_type: originalPage.item_type,
|
|
|
|
|
sections: [], // 섹션은 별도 API로 복제해야 함
|
|
|
|
|
is_active: true,
|
|
|
|
|
absolute_path: absolutePath,
|
|
|
|
|
order_no: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 서버에서 반환된 ID로 선택
|
|
|
|
|
setSelectedPageId(newPage.id);
|
|
|
|
|
toast.success('페이지가 복제되었습니다');
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// TODO: 원본 페이지의 섹션들도 복제 필요 (별도 API 호출)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const errorMessage = getErrorMessage(err);
|
|
|
|
|
toast.error(errorMessage);
|
|
|
|
|
console.error('❌ Failed to duplicate page:', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddSection = () => {
|
|
|
|
|
if (!selectedPage || !newSectionTitle.trim()) return toast.error('하위섹션 제목을 입력해주세요');
|
2025-11-23 16:10:27 +09:00
|
|
|
const sectionType: 'BASIC' | 'BOM' | 'CUSTOM' = newSectionType === 'bom' ? 'BOM' : 'BASIC';
|
2025-11-18 14:17:52 +09:00
|
|
|
const newSection: ItemSection = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
page_id: selectedPage.id,
|
|
|
|
|
section_name: newSectionTitle,
|
|
|
|
|
section_type: sectionType,
|
2025-11-18 14:17:52 +09:00
|
|
|
description: newSectionDescription || undefined,
|
2025-11-23 16:10:27 +09:00
|
|
|
order_no: selectedPage.sections.length + 1,
|
|
|
|
|
is_collapsible: true,
|
|
|
|
|
is_default_open: true,
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString(),
|
2025-11-18 14:17:52 +09:00
|
|
|
fields: [],
|
2025-11-23 16:10:27 +09:00
|
|
|
bomItems: sectionType === 'BOM' ? [] : undefined
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('Adding section to page:', {
|
|
|
|
|
pageId: selectedPage.id,
|
2025-11-23 16:10:27 +09:00
|
|
|
page_name: selectedPage.page_name,
|
|
|
|
|
sectionTitle: newSection.section_name,
|
|
|
|
|
sectionType: newSection.section_type,
|
2025-11-18 14:17:52 +09:00
|
|
|
currentSectionCount: selectedPage.sections.length,
|
|
|
|
|
newSection: newSection
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 1. 페이지에 섹션 추가
|
|
|
|
|
addSectionToPage(selectedPage.id, newSection);
|
2025-11-23 16:10:27 +09:00
|
|
|
// 섹션은 페이지의 일부이므로 sections로 별도 추적하지 않음
|
|
|
|
|
|
|
|
|
|
// 2. 섹션관리 탭에도 템플릿으로 자동 추가 (계층구조 섹션 = 섹션 탭 섹션)
|
2025-11-25 21:07:10 +09:00
|
|
|
// 프론트엔드 형식: template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
|
|
|
|
const newTemplateData = {
|
|
|
|
|
tenant_id: tenantId ?? 0,
|
2025-11-23 16:10:27 +09:00
|
|
|
template_name: newSection.section_name,
|
2025-11-25 21:07:10 +09:00
|
|
|
section_type: newSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM',
|
|
|
|
|
description: newSection.description ?? null,
|
2025-11-23 16:10:27 +09:00
|
|
|
default_fields: null,
|
2025-11-25 21:07:10 +09:00
|
|
|
created_by: null,
|
|
|
|
|
updated_by: null,
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
2025-11-25 21:07:10 +09:00
|
|
|
addSectionTemplate(newTemplateData);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
console.log('Section added to both page and template:', {
|
|
|
|
|
sectionId: newSection.id,
|
2025-11-25 21:07:10 +09:00
|
|
|
templateTitle: newTemplateData.title
|
2025-11-18 14:17:52 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setNewSectionTitle('');
|
|
|
|
|
setNewSectionDescription('');
|
|
|
|
|
setNewSectionType('fields');
|
|
|
|
|
setIsSectionDialogOpen(false);
|
|
|
|
|
toast.success(`${newSectionType === 'bom' ? 'BOM' : '일반'} 섹션이 페이지에 추가되고 템플릿으로도 등록되었습니다!`);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 섹션 템플릿을 페이지에 연결 (SectionDialog에서 사용)
|
|
|
|
|
const handleLinkTemplate = (template: SectionTemplate) => {
|
|
|
|
|
if (!selectedPage) {
|
|
|
|
|
toast.error('페이지를 먼저 선택해주세요');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 템플릿을 섹션으로 변환하여 페이지에 추가
|
|
|
|
|
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
|
|
|
|
|
page_id: selectedPage.id,
|
|
|
|
|
section_name: template.template_name,
|
|
|
|
|
section_type: template.section_type,
|
|
|
|
|
description: template.description || undefined,
|
|
|
|
|
order_no: selectedPage.sections.length + 1,
|
|
|
|
|
is_collapsible: true,
|
|
|
|
|
is_default_open: true,
|
|
|
|
|
fields: template.fields ? template.fields.map((field, idx) => ({
|
|
|
|
|
id: Date.now() + idx,
|
|
|
|
|
section_id: 0, // 추후 업데이트됨
|
|
|
|
|
field_name: field.name,
|
|
|
|
|
field_type: field.property.inputType,
|
|
|
|
|
order_no: idx + 1,
|
|
|
|
|
is_required: field.property.required,
|
|
|
|
|
placeholder: field.description || null,
|
|
|
|
|
default_value: null,
|
|
|
|
|
display_condition: null,
|
|
|
|
|
validation_rules: null,
|
|
|
|
|
options: field.property.options
|
|
|
|
|
? field.property.options.map(opt => ({ label: opt, value: opt }))
|
|
|
|
|
: null,
|
|
|
|
|
properties: field.property.multiColumn ? {
|
|
|
|
|
multiColumn: true,
|
|
|
|
|
columnCount: field.property.columnCount,
|
|
|
|
|
columnNames: field.property.columnNames
|
|
|
|
|
} : null,
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString()
|
|
|
|
|
})) : [],
|
|
|
|
|
bomItems: template.section_type === 'BOM' ? (template.bomItems || []) : undefined
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('Linking template to page:', {
|
|
|
|
|
templateId: template.id,
|
|
|
|
|
templateName: template.template_name,
|
|
|
|
|
pageId: selectedPage.id,
|
|
|
|
|
newSection
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
addSectionToPage(selectedPage.id, newSection);
|
|
|
|
|
|
|
|
|
|
// 다이얼로그 상태 초기화
|
|
|
|
|
setSectionInputMode('custom');
|
|
|
|
|
setSelectedSectionTemplateId(null);
|
|
|
|
|
setNewSectionTitle('');
|
|
|
|
|
setNewSectionDescription('');
|
|
|
|
|
setNewSectionType('fields');
|
|
|
|
|
setIsSectionDialogOpen(false);
|
|
|
|
|
|
|
|
|
|
toast.success(`"${template.template_name}" 템플릿이 페이지에 연결되었습니다!`);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const handleEditSectionTitle = (sectionId: string, currentTitle: string) => {
|
|
|
|
|
setEditingSectionId(sectionId);
|
|
|
|
|
setEditingSectionTitle(currentTitle);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSaveSectionTitle = () => {
|
|
|
|
|
if (!selectedPage || !editingSectionId || !editingSectionTitle.trim())
|
|
|
|
|
return toast.error('하위섹션 제목을 입력해주세요');
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
updateSection(Number(editingSectionId), { section_name: editingSectionTitle });
|
2025-11-18 14:17:52 +09:00
|
|
|
setEditingSectionId(null);
|
|
|
|
|
toast.success('하위섹션 제목이 수정되었습니다 (저장 필요)');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleMoveSectionUp = (sectionId: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!selectedPage) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const sections = [...selectedPage.sections];
|
|
|
|
|
const index = sections.findIndex(s => s.id === sectionId);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (index <= 0) return; // 첫 번째 섹션이거나 못 찾음
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 배열에서 위치 교환
|
|
|
|
|
[sections[index - 1], sections[index]] = [sections[index], sections[index - 1]];
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// order 값 재설정
|
|
|
|
|
const updatedSections = sections.map((section, idx) => ({
|
|
|
|
|
...section,
|
|
|
|
|
order: idx + 1
|
|
|
|
|
}));
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 페이지 업데이트
|
|
|
|
|
updateItemPage(selectedPage.id, { sections: updatedSections });
|
|
|
|
|
toast.success('섹션 순서가 변경되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleMoveSectionDown = (sectionId: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!selectedPage) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const sections = [...selectedPage.sections];
|
|
|
|
|
const index = sections.findIndex(s => s.id === sectionId);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (index < 0 || index >= sections.length - 1) return; // 마지막 섹션이거나 못 찾음
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 배열에서 위치 교환
|
|
|
|
|
[sections[index], sections[index + 1]] = [sections[index + 1], sections[index]];
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// order 값 재설정
|
|
|
|
|
const updatedSections = sections.map((section, idx) => ({
|
|
|
|
|
...section,
|
|
|
|
|
order: idx + 1
|
|
|
|
|
}));
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 페이지 업데이트
|
|
|
|
|
updateItemPage(selectedPage.id, { 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;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 마스터 항목에서 가져온 경우 master_field_id 설정
|
|
|
|
|
const masterFieldId = fieldInputMode === 'master' && selectedMasterFieldId
|
|
|
|
|
? Number(selectedMasterFieldId)
|
|
|
|
|
: null;
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newField: ItemField = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: editingFieldId ? Number(editingFieldId) : Date.now(),
|
|
|
|
|
section_id: Number(selectedSectionForField),
|
2025-11-25 21:07:10 +09:00
|
|
|
master_field_id: masterFieldId, // 마스터 항목 연결 정보
|
2025-11-23 16:10:27 +09:00
|
|
|
field_name: newFieldName,
|
|
|
|
|
field_type: newFieldInputType,
|
|
|
|
|
order_no: 0,
|
|
|
|
|
is_required: newFieldRequired,
|
|
|
|
|
placeholder: newFieldDescription || null,
|
|
|
|
|
default_value: null,
|
|
|
|
|
display_condition: displayCondition as Record<string, any> | null || null,
|
|
|
|
|
validation_rules: null,
|
|
|
|
|
options: newFieldInputType === 'dropdown' && newFieldOptions.trim()
|
|
|
|
|
? newFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
|
|
|
|
: null,
|
|
|
|
|
properties: hasColumns ? {
|
|
|
|
|
multiColumn: true,
|
|
|
|
|
columnCount: textboxColumns.length,
|
|
|
|
|
columnNames: textboxColumns.map(c => c.name)
|
|
|
|
|
} : null,
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString()
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (editingFieldId) {
|
2025-11-23 16:10:27 +09:00
|
|
|
console.log('Updating field:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldId: editingFieldId, fieldName: newField.field_name });
|
|
|
|
|
updateField(Number(editingFieldId), newField);
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 항목관리 탭의 마스터 항목도 업데이트 (동일한 fieldKey가 있으면)
|
2025-11-23 16:10:27 +09:00
|
|
|
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
|
2025-11-18 14:17:52 +09:00
|
|
|
if (existingMasterField) {
|
|
|
|
|
const updatedMasterField: ItemMasterField = {
|
|
|
|
|
...existingMasterField,
|
2025-11-23 16:10:27 +09:00
|
|
|
field_name: newField.field_name,
|
|
|
|
|
description: newField.placeholder,
|
|
|
|
|
default_properties: newField.properties,
|
|
|
|
|
updated_at: new Date().toISOString()
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
updateItemMasterField(existingMasterField.id, updatedMasterField);
|
|
|
|
|
}
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('항목이 섹션에 수정되었습니다!');
|
|
|
|
|
} else {
|
2025-11-23 16:10:27 +09:00
|
|
|
console.log('Adding field to section:', { pageId: selectedPage.id, sectionId: selectedSectionForField, fieldName: newField.field_name });
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 1. 섹션에 항목 추가
|
2025-11-23 16:10:27 +09:00
|
|
|
addFieldToSection(Number(selectedSectionForField), newField);
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// 2. 마스터 항목 선택이 아닌 경우에만 새 마스터 항목 자동 생성
|
|
|
|
|
// (마스터 항목 선택 시에는 이미 master_field_id로 연결되어 있음)
|
|
|
|
|
const isFromMasterField = masterFieldId !== null;
|
2025-11-23 16:10:27 +09:00
|
|
|
const existingMasterField = itemMasterFields.find(mf => mf.id.toString() === newField.field_name);
|
2025-11-25 21:07:10 +09:00
|
|
|
if (!isFromMasterField && !existingMasterField) {
|
|
|
|
|
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
2025-11-18 14:17:52 +09:00
|
|
|
const newMasterField: ItemMasterField = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
field_name: newField.field_name,
|
2025-11-25 21:07:10 +09:00
|
|
|
field_type: newField.field_type, // API 스펙에 맞게 소문자 그대로 전달
|
2025-11-23 16:10:27 +09:00
|
|
|
description: newField.placeholder,
|
|
|
|
|
default_properties: newField.properties,
|
|
|
|
|
category: selectedPage.item_type, // 현재 페이지의 품목유형을 카테고리로 설정
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString()
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
addItemMasterField(newMasterField);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
console.log('Field added to both section and master fields:', {
|
|
|
|
|
fieldId: newField.id,
|
2025-11-23 16:10:27 +09:00
|
|
|
masterFieldId: newMasterField.id
|
2025-11-18 14:17:52 +09:00
|
|
|
});
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 3. dropdown 타입이고 옵션이 있으면 속성관리 탭에도 자동 추가
|
2025-11-23 16:10:27 +09:00
|
|
|
if (newField.properties?.inputType === 'dropdown' && newField.options && newField.options.length > 0) {
|
|
|
|
|
const existingCustomOptions = customAttributeOptions[newField.field_name];
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!existingCustomOptions || existingCustomOptions.length === 0) {
|
2025-11-23 16:10:27 +09:00
|
|
|
const customOptions = newField.options.map((option: { label: string; value: string }, index: number) => ({
|
|
|
|
|
id: `CUSTOM-${newField.field_name}-${Date.now()}-${index}`,
|
|
|
|
|
value: option.value,
|
|
|
|
|
label: option.label,
|
2025-11-18 14:17:52 +09:00
|
|
|
isActive: true
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setCustomAttributeOptions(prev => ({
|
|
|
|
|
...prev,
|
2025-11-23 16:10:27 +09:00
|
|
|
[newField.field_name]: customOptions
|
2025-11-18 14:17:52 +09:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 속성관리 탭에 하위 탭으로 추가
|
2025-11-23 16:10:27 +09:00
|
|
|
const existingTab = attributeSubTabs.find(tab => tab.key === newField.field_name);
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!existingTab) {
|
|
|
|
|
const maxOrder = Math.max(...attributeSubTabs.map(t => t.order), -1);
|
|
|
|
|
const newTab = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: `attr-${newField.field_name}`,
|
|
|
|
|
label: newField.field_name,
|
|
|
|
|
key: newField.field_name,
|
2025-11-18 14:17:52 +09:00
|
|
|
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:', {
|
2025-11-23 16:10:27 +09:00
|
|
|
attributeKey: newField.field_name,
|
2025-11-18 14:17:52 +09:00
|
|
|
options: customOptions
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
toast.success(`항목이 추가되고 "${newField.field_name}" 속성 탭이 속성관리에 등록되었습니다!`);
|
2025-11-18 14:17:52 +09:00
|
|
|
} 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) => {
|
2025-11-23 16:10:27 +09:00
|
|
|
setSelectedSectionForField(Number(sectionId));
|
2025-11-18 14:17:52 +09:00
|
|
|
setEditingFieldId(field.id);
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewFieldName(field.field_name);
|
|
|
|
|
setNewFieldKey(field.id.toString());
|
|
|
|
|
setNewFieldInputType(field.field_type);
|
|
|
|
|
setNewFieldRequired(field.is_required);
|
|
|
|
|
setNewFieldOptions(field.options?.map(opt => opt.value).join(', ') || '');
|
|
|
|
|
setNewFieldDescription(''); // description은 ItemField에 없음
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 조건부 표시 설정 로드
|
2025-11-23 16:10:27 +09:00
|
|
|
if (field.display_condition) {
|
2025-11-18 14:17:52 +09:00
|
|
|
setNewFieldConditionEnabled(true);
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewFieldConditionTargetType(field.display_condition.targetType);
|
|
|
|
|
setNewFieldConditionFields(field.display_condition.fieldConditions || []);
|
|
|
|
|
setNewFieldConditionSections(field.display_condition.sectionIds || []);
|
2025-11-18 14:17:52 +09:00
|
|
|
} else {
|
|
|
|
|
setNewFieldConditionEnabled(false);
|
|
|
|
|
setNewFieldConditionTargetType('field');
|
|
|
|
|
setNewFieldConditionFields([]);
|
|
|
|
|
setNewFieldConditionSections([]);
|
|
|
|
|
}
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setIsFieldDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 마스터 필드 선택 시 폼 자동 채우기
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (fieldInputMode === 'master' && selectedMasterFieldId) {
|
2025-11-23 16:10:27 +09:00
|
|
|
const masterField = itemMasterFields.find(f => f.id === Number(selectedMasterFieldId));
|
2025-11-18 14:17:52 +09:00
|
|
|
if (masterField) {
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewFieldName(masterField.field_name);
|
|
|
|
|
setNewFieldKey(masterField.id.toString());
|
|
|
|
|
setNewFieldInputType(masterField.default_properties?.inputType);
|
|
|
|
|
setNewFieldRequired(masterField.default_properties?.required);
|
|
|
|
|
setNewFieldOptions(masterField.default_properties?.options?.join(', ') || '');
|
2025-11-18 14:17:52 +09:00
|
|
|
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 타입이고 옵션이 있으면 각 옵션을 속성으로 추가)
|
2025-11-23 16:10:27 +09:00
|
|
|
let _properties: ItemFieldProperty[] = [];
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) {
|
|
|
|
|
const options = newMasterFieldOptions.split(',').map(o => o.trim());
|
2025-11-23 16:10:27 +09:00
|
|
|
_properties = options.map((opt, idx) => ({
|
2025-11-18 14:17:52 +09:00
|
|
|
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
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
2025-11-18 14:17:52 +09:00
|
|
|
const newMasterField: ItemMasterField = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
field_name: newMasterFieldName,
|
2025-11-25 21:07:10 +09:00
|
|
|
field_type: newMasterFieldInputType,
|
2025-11-23 16:10:27 +09:00
|
|
|
category: newMasterFieldCategory || null,
|
|
|
|
|
description: newMasterFieldDescription || null,
|
|
|
|
|
default_validation: null,
|
|
|
|
|
default_properties: {
|
2025-11-18 14:17:52 +09:00
|
|
|
inputType: newMasterFieldInputType,
|
|
|
|
|
required: newMasterFieldRequired,
|
|
|
|
|
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
|
2025-11-23 16:10:27 +09:00
|
|
|
},
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString()
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addItemMasterField(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);
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewMasterFieldName(field.field_name);
|
|
|
|
|
setNewMasterFieldKey(field.id.toString());
|
|
|
|
|
setNewMasterFieldInputType(field.default_properties?.inputType);
|
|
|
|
|
setNewMasterFieldRequired(field.default_properties?.required);
|
2025-11-18 14:17:52 +09:00
|
|
|
setNewMasterFieldCategory(field.category || '공통');
|
|
|
|
|
setNewMasterFieldDescription(field.description || '');
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewMasterFieldOptions(field.default_properties?.options?.join(', ') || '');
|
|
|
|
|
setNewMasterFieldAttributeType((field.default_properties as any).attributeType || 'custom');
|
|
|
|
|
setNewMasterFieldMultiColumn(field.default_properties?.multiColumn || false);
|
|
|
|
|
setNewMasterFieldColumnCount(field.default_properties?.columnCount || 2);
|
|
|
|
|
setNewMasterFieldColumnNames(field.default_properties?.columnNames || ['컬럼1', '컬럼2']);
|
2025-11-18 14:17:52 +09:00
|
|
|
setIsMasterFieldDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpdateMasterField = () => {
|
|
|
|
|
if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim())
|
|
|
|
|
return toast.error('항목명과 필드 키를 입력해주세요');
|
|
|
|
|
|
|
|
|
|
// 속성 목록 업데이트 (dropdown 타입이고 옵션이 있으면 각 옵션을 속성으로 추가)
|
2025-11-23 16:10:27 +09:00
|
|
|
let _properties2: ItemFieldProperty[] = [];
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (newMasterFieldInputType === 'dropdown' && newMasterFieldOptions.trim()) {
|
|
|
|
|
const options = newMasterFieldOptions.split(',').map(o => o.trim());
|
2025-11-23 16:10:27 +09:00
|
|
|
_properties2 = options.map((opt, idx) => ({
|
2025-11-18 14:17:52 +09:00
|
|
|
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, {
|
2025-11-23 16:10:27 +09:00
|
|
|
field_name: newMasterFieldName,
|
|
|
|
|
default_properties: {
|
2025-11-18 14:17:52 +09:00
|
|
|
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
|
2025-11-23 16:10:27 +09:00
|
|
|
},
|
2025-11-18 14:17:52 +09:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
toast.success('마스터 항목이 수정되었습니다 (속성 탭에 반영됨, 저장 필요)');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleDeleteMasterField = (id: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (confirm('이 마스터 항목을 삭제하시겠습니까?')) {
|
|
|
|
|
// 삭제할 마스터 항목 찾기
|
|
|
|
|
const fieldToDelete = itemMasterFields.find(f => f.id === id);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 마스터 항목 삭제
|
|
|
|
|
deleteItemMasterField(id);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 속성 탭에서 해당 탭 제거
|
|
|
|
|
if (fieldToDelete) {
|
2025-11-23 16:10:27 +09:00
|
|
|
setAttributeSubTabs(prev => prev.filter(tab => tab.key !== fieldToDelete.id.toString()));
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 삭제된 탭이 현재 활성 탭이면 다른 탭으로 전환
|
2025-11-23 16:10:27 +09:00
|
|
|
if (activeAttributeTab === fieldToDelete.id.toString()) {
|
2025-11-18 14:17:52 +09:00
|
|
|
setActiveAttributeTab('units');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('마스터 항목이 삭제되었습니다');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 페이지 삭제 핸들러 (pendingChanges 제거 포함)
|
|
|
|
|
const handleDeletePageWithTracking = (pageId: number) => {
|
|
|
|
|
// 삭제할 페이지 찾기
|
|
|
|
|
const pageToDelete = itemPages.find(p => p.id === pageId);
|
|
|
|
|
|
|
|
|
|
// 해당 페이지의 모든 섹션 ID 수집
|
|
|
|
|
const sectionIds = pageToDelete?.sections.map(s => s.id) || [];
|
|
|
|
|
|
|
|
|
|
// 해당 페이지의 모든 필드 ID 수집
|
|
|
|
|
const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
|
|
|
|
|
|
|
|
|
|
// ItemMasterContext의 deleteItemPage 호출
|
|
|
|
|
deleteItemPage(pageId);
|
|
|
|
|
|
|
|
|
|
console.log('페이지 삭제 완료:', {
|
|
|
|
|
pageId,
|
|
|
|
|
removedSections: sectionIds.length,
|
|
|
|
|
removedFields: fieldIds.length
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 섹션 삭제 핸들러 (pendingChanges 제거 포함)
|
|
|
|
|
const handleDeleteSectionWithTracking = (pageId: number, sectionId: number) => {
|
|
|
|
|
// 삭제할 섹션 찾기
|
|
|
|
|
const page = itemPages.find(p => p.id === pageId);
|
|
|
|
|
const sectionToDelete = page?.sections.find(s => s.id === sectionId);
|
|
|
|
|
|
|
|
|
|
// 해당 섹션의 모든 필드 ID 수집
|
|
|
|
|
const fieldIds = sectionToDelete?.fields?.map(f => f.id) || [];
|
|
|
|
|
|
|
|
|
|
// ItemMasterContext의 deleteSection 호출
|
|
|
|
|
deleteSection(Number(sectionId));
|
|
|
|
|
|
|
|
|
|
console.log('섹션 삭제 완료:', {
|
|
|
|
|
sectionId,
|
|
|
|
|
removedFields: fieldIds.length
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 삭제 핸들러 (pendingChanges 제거 포함)
|
|
|
|
|
const handleDeleteFieldWithTracking = (pageId: string, sectionId: string, fieldId: string) => {
|
|
|
|
|
// ItemMasterContext의 deleteField 호출
|
|
|
|
|
deleteField(Number(fieldId));
|
|
|
|
|
|
|
|
|
|
console.log('필드 삭제 완료:', fieldId);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 섹션 템플릿 핸들러
|
|
|
|
|
const handleAddSectionTemplate = () => {
|
2025-11-25 21:07:10 +09:00
|
|
|
if (!newSectionTemplateTitle.trim())
|
2025-11-18 14:17:52 +09:00
|
|
|
return toast.error('섹션 제목을 입력해주세요');
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// Context의 addSectionTemplate이 기대하는 SectionTemplate 형식 사용
|
|
|
|
|
// template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
|
|
|
|
const newTemplateData = {
|
|
|
|
|
tenant_id: tenantId ?? 0,
|
2025-11-23 16:10:27 +09:00
|
|
|
template_name: newSectionTemplateTitle,
|
2025-11-25 21:07:10 +09:00
|
|
|
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM',
|
|
|
|
|
description: newSectionTemplateDescription || null,
|
2025-11-23 16:10:27 +09:00
|
|
|
default_fields: null,
|
2025-11-25 21:07:10 +09:00
|
|
|
category: newSectionTemplateCategory,
|
|
|
|
|
created_by: null,
|
|
|
|
|
updated_by: null,
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
console.log('Adding section template:', newTemplateData);
|
|
|
|
|
addSectionTemplate(newTemplateData);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setNewSectionTemplateTitle('');
|
|
|
|
|
setNewSectionTemplateDescription('');
|
|
|
|
|
setNewSectionTemplateCategory([]);
|
|
|
|
|
setNewSectionTemplateType('fields');
|
|
|
|
|
setIsSectionTemplateDialogOpen(false);
|
|
|
|
|
toast.success('섹션 템플릿이 추가되었습니다! (템플릿 목록에서 확인 가능)');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditSectionTemplate = (template: SectionTemplate) => {
|
|
|
|
|
setEditingSectionTemplateId(template.id);
|
2025-11-25 21:07:10 +09:00
|
|
|
// SectionTemplate 타입에 맞게 template_name, section_type 사용
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewSectionTemplateTitle(template.template_name);
|
2025-11-18 14:17:52 +09:00
|
|
|
setNewSectionTemplateDescription(template.description || '');
|
2025-11-25 21:07:10 +09:00
|
|
|
setNewSectionTemplateCategory(template.category || []);
|
2025-11-23 16:10:27 +09:00
|
|
|
setNewSectionTemplateType(template.section_type === 'BOM' ? 'bom' : 'fields');
|
2025-11-18 14:17:52 +09:00
|
|
|
setIsSectionTemplateDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpdateSectionTemplate = () => {
|
2025-11-23 16:10:27 +09:00
|
|
|
if (!editingSectionTemplateId || !newSectionTemplateTitle.trim())
|
2025-11-18 14:17:52 +09:00
|
|
|
return toast.error('섹션 제목을 입력해주세요');
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// Context의 updateSectionTemplate이 기대하는 SectionTemplate 형식 사용
|
|
|
|
|
// template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
2025-11-23 16:10:27 +09:00
|
|
|
const updateData = {
|
2025-11-25 21:07:10 +09:00
|
|
|
template_name: newSectionTemplateTitle,
|
2025-11-18 14:17:52 +09:00
|
|
|
description: newSectionTemplateDescription || undefined,
|
|
|
|
|
category: newSectionTemplateCategory.length > 0 ? newSectionTemplateCategory : undefined,
|
2025-11-25 21:07:10 +09:00
|
|
|
section_type: (newSectionTemplateType === 'bom' ? 'BOM' : 'BASIC') as 'BASIC' | 'BOM' | 'CUSTOM'
|
2025-11-23 16:10:27 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
console.log('Updating section template:', { id: editingSectionTemplateId, updateData });
|
2025-11-23 16:10:27 +09:00
|
|
|
updateSectionTemplate(editingSectionTemplateId, updateData);
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
setEditingSectionTemplateId(null);
|
|
|
|
|
setNewSectionTemplateTitle('');
|
|
|
|
|
setNewSectionTemplateDescription('');
|
|
|
|
|
setNewSectionTemplateCategory([]);
|
|
|
|
|
setNewSectionTemplateType('fields');
|
|
|
|
|
setIsSectionTemplateDialogOpen(false);
|
2025-11-23 16:10:27 +09:00
|
|
|
toast.success('섹션이 수정되었습니다 (저장 필요)');
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleDeleteSectionTemplate = (id: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (confirm('이 섹션을 삭제하시겠습니까?')) {
|
2025-11-23 16:10:27 +09:00
|
|
|
// 섹션 템플릿 삭제
|
2025-11-18 14:17:52 +09:00
|
|
|
deleteSectionTemplate(id);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('섹션이 삭제되었습니다');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 섹션 템플릿 불러오기
|
|
|
|
|
const handleLoadTemplate = () => {
|
|
|
|
|
if (!selectedTemplateId || !selectedPage) {
|
|
|
|
|
return toast.error('템플릿을 선택해주세요');
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const template = sectionTemplates.find(t => t.id === Number(selectedTemplateId));
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!template) {
|
|
|
|
|
return toast.error('템플릿을 찾을 수 없습니다');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 템플릿을 복사해서 섹션으로 추가
|
2025-11-25 21:07:10 +09:00
|
|
|
// API 스펙: SectionTemplate은 title, type ('fields' | 'bom') 사용
|
2025-11-23 16:10:27 +09:00
|
|
|
const newSection: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> = {
|
|
|
|
|
page_id: selectedPage.id,
|
2025-11-25 21:07:10 +09:00
|
|
|
section_name: template.title,
|
|
|
|
|
section_type: template.type === 'bom' ? 'BOM' : 'BASIC',
|
2025-11-23 16:10:27 +09:00
|
|
|
description: template.description || undefined,
|
|
|
|
|
order_no: selectedPage.sections.length + 1,
|
|
|
|
|
is_collapsible: true,
|
|
|
|
|
is_default_open: true,
|
|
|
|
|
fields: [],
|
2025-11-25 21:07:10 +09:00
|
|
|
bomItems: template.type === 'bom' ? [] : undefined
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
console.log('Loading template to section:', template.title, 'type:', template.type, 'newSection:', newSection);
|
2025-11-18 14:17:52 +09:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 항목 탭에 해당 항목이 없으면 자동으로 추가
|
2025-11-23 16:10:27 +09:00
|
|
|
const existingMasterField = itemMasterFields.find(f => f.id.toString() === templateFieldKey);
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!existingMasterField && !editingTemplateFieldId) {
|
2025-11-25 21:07:10 +09:00
|
|
|
// API 스펙: field_type은 소문자만 허용 (textbox, number, dropdown, checkbox, date, textarea)
|
2025-11-18 14:17:52 +09:00
|
|
|
const newMasterField: ItemMasterField = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
field_name: templateFieldName,
|
2025-11-25 21:07:10 +09:00
|
|
|
field_type: templateFieldInputType,
|
2025-11-23 16:10:27 +09:00
|
|
|
default_properties: {
|
2025-11-18 14:17:52 +09:00
|
|
|
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,
|
2025-11-23 16:10:27 +09:00
|
|
|
created_at: new Date().toISOString().split('T')[0],
|
|
|
|
|
updated_at: new Date().toISOString().split('T')[0]
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
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('항목 탭에 자동으로 추가되었습니다');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// TemplateField 형식으로 생성 (UI가 기대하는 형식)
|
|
|
|
|
const newField: TemplateField = {
|
|
|
|
|
id: String(editingTemplateFieldId || Date.now()),
|
|
|
|
|
name: templateFieldName,
|
|
|
|
|
fieldKey: templateFieldKey,
|
|
|
|
|
property: {
|
2025-11-18 14:17:52 +09:00
|
|
|
inputType: templateFieldInputType,
|
|
|
|
|
required: templateFieldRequired,
|
2025-11-25 21:07:10 +09:00
|
|
|
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
|
|
|
|
? templateFieldOptions.split(',').map(o => o.trim())
|
|
|
|
|
: undefined,
|
2025-11-18 14:17:52 +09:00
|
|
|
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
|
|
|
|
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
|
|
|
|
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined
|
|
|
|
|
},
|
2025-11-25 21:07:10 +09:00
|
|
|
description: templateFieldDescription || undefined
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let updatedFields;
|
2025-11-23 16:10:27 +09:00
|
|
|
const currentFields = template.default_fields ? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields) : [];
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
if (editingTemplateFieldId) {
|
2025-11-25 21:07:10 +09:00
|
|
|
// f.id는 string, editingTemplateFieldId는 number이므로 String으로 변환하여 비교
|
|
|
|
|
updatedFields = Array.isArray(currentFields) ? currentFields.map((f: any) => String(f.id) === String(editingTemplateFieldId) ? newField : f) : [];
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('항목이 수정되었습니다');
|
|
|
|
|
} else {
|
2025-11-23 16:10:27 +09:00
|
|
|
updatedFields = Array.isArray(currentFields) ? [...currentFields, newField] : [newField];
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('항목이 추가되었습니다');
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
updateSectionTemplate(currentTemplateId, { default_fields: updatedFields });
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
// 폼 초기화
|
|
|
|
|
setTemplateFieldName('');
|
|
|
|
|
setTemplateFieldKey('');
|
|
|
|
|
setTemplateFieldInputType('textbox');
|
|
|
|
|
setTemplateFieldRequired(false);
|
|
|
|
|
setTemplateFieldOptions('');
|
|
|
|
|
setTemplateFieldDescription('');
|
|
|
|
|
setTemplateFieldMultiColumn(false);
|
|
|
|
|
setTemplateFieldColumnCount(2);
|
|
|
|
|
setTemplateFieldColumnNames(['컬럼1', '컬럼2']);
|
|
|
|
|
setEditingTemplateFieldId(null);
|
|
|
|
|
setIsTemplateFieldDialogOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
// TemplateField 형식으로 수정 (UI가 전달하는 형식)
|
|
|
|
|
const handleEditTemplateField = (templateId: number, field: TemplateField) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
setCurrentTemplateId(templateId);
|
2025-11-25 21:07:10 +09:00
|
|
|
setEditingTemplateFieldId(Number(field.id)); // TemplateField.id는 string
|
|
|
|
|
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']);
|
2025-11-18 14:17:52 +09:00
|
|
|
setIsTemplateFieldDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleDeleteTemplateField = (templateId: number, fieldId: string) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
|
|
|
|
|
|
|
|
|
const template = sectionTemplates.find(t => t.id === templateId);
|
|
|
|
|
if (!template) return;
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const currentFields = template.default_fields ? (typeof template.default_fields === 'string' ? JSON.parse(template.default_fields) : template.default_fields) : [];
|
2025-11-25 21:07:10 +09:00
|
|
|
// f.id는 number 또는 string일 수 있으므로 String으로 변환하여 비교
|
|
|
|
|
const updatedFields = Array.isArray(currentFields) ? currentFields.filter((f: any) => String(f.id) !== String(fieldId)) : [];
|
2025-11-23 16:10:27 +09:00
|
|
|
updateSectionTemplate(templateId, { default_fields: updatedFields });
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('항목이 삭제되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// BOM 관리 핸들러
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleAddBOMItem = (item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
const newItem: BOMItem = {
|
|
|
|
|
...item,
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString(),
|
2025-11-25 21:07:10 +09:00
|
|
|
tenant_id: tenantId ?? 0,
|
2025-11-23 16:10:27 +09:00
|
|
|
section_id: 0
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
setBomItems(prev => [...prev, newItem]);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleUpdateBOMItem = (id: number, item: Partial<BOMItem>) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
setBomItems(prev => prev.map(bom => bom.id === id ? { ...bom, ...item } : bom));
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleDeleteBOMItem = (id: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
setBomItems(prev => prev.filter(bom => bom.id !== id));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 템플릿별 BOM 관리 핸들러
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleAddBOMItemToTemplate = (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
const newItem: BOMItem = {
|
|
|
|
|
...item,
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now(),
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
updated_at: new Date().toISOString(),
|
2025-11-25 21:07:10 +09:00
|
|
|
tenant_id: tenantId ?? 0,
|
2025-11-23 16:10:27 +09:00
|
|
|
section_id: 0
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const template = sectionTemplates.find(t => t.id === templateId);
|
|
|
|
|
if (!template) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const updatedBomItems = [...(template.bomItems || []), newItem];
|
|
|
|
|
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
const template = sectionTemplates.find(t => t.id === templateId);
|
|
|
|
|
if (!template || !template.bomItems) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
const updatedBomItems = template.bomItems.map(bom =>
|
2025-11-18 14:17:52 +09:00
|
|
|
bom.id === itemId ? { ...bom, ...item } : bom
|
|
|
|
|
);
|
|
|
|
|
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const handleDeleteBOMItemFromTemplate = (templateId: number, itemId: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
const template = sectionTemplates.find(t => t.id === templateId);
|
|
|
|
|
if (!template || !template.bomItems) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
|
|
|
|
|
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _toggleSection = (sectionId: string) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
setExpandedSections(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 탭 관리 함수
|
|
|
|
|
const handleAddTab = () => {
|
|
|
|
|
if (!newTabLabel.trim()) return toast.error('탭 이름을 입력해주세요');
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newTab = {
|
2025-11-23 16:10:27 +09:00
|
|
|
id: Date.now().toString(),
|
2025-11-18 14:17:52 +09:00
|
|
|
label: newTabLabel,
|
|
|
|
|
icon: 'FileText',
|
|
|
|
|
isDefault: false,
|
|
|
|
|
order: customTabs.length + 1
|
|
|
|
|
};
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setCustomTabs(prev => [...prev, newTab]);
|
|
|
|
|
setNewTabLabel('');
|
|
|
|
|
setIsAddTabDialogOpen(false);
|
|
|
|
|
toast.success('탭이 추가되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
const _handleEditTab = (tabId: string) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
const tab = customTabs.find(t => t.id === tabId);
|
|
|
|
|
if (!tab || tab.isDefault) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
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;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newTabs = [...attributeSubTabs];
|
|
|
|
|
const temp = newTabs[tabIndex - 1].order;
|
|
|
|
|
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
|
|
|
|
newTabs[tabIndex].order = temp;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
2025-11-23 16:10:27 +09:00
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const moveAttributeTabDown = (tabId: string) => {
|
|
|
|
|
const tabIndex = attributeSubTabs.findIndex(t => t.id === tabId);
|
|
|
|
|
if (tabIndex >= attributeSubTabs.length - 1) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newTabs = [...attributeSubTabs];
|
|
|
|
|
const temp = newTabs[tabIndex + 1].order;
|
|
|
|
|
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
|
|
|
|
newTabs[tabIndex].order = temp;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setAttributeSubTabs(newTabs.sort((a, b) => a.order - b.order));
|
2025-11-23 16:10:27 +09:00
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newTabs = [...customTabs];
|
|
|
|
|
const temp = newTabs[tabIndex - 1].order;
|
|
|
|
|
newTabs[tabIndex - 1].order = newTabs[tabIndex].order;
|
|
|
|
|
newTabs[tabIndex].order = temp;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
2025-11-23 16:10:27 +09:00
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('탭 순서가 변경되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const moveTabDown = (tabId: string) => {
|
|
|
|
|
const tabIndex = customTabs.findIndex(t => t.id === tabId);
|
|
|
|
|
if (tabIndex >= customTabs.length - 1) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const newTabs = [...customTabs];
|
|
|
|
|
const temp = newTabs[tabIndex + 1].order;
|
|
|
|
|
newTabs[tabIndex + 1].order = newTabs[tabIndex].order;
|
|
|
|
|
newTabs[tabIndex].order = temp;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
setCustomTabs(newTabs.sort((a, b) => a.order - b.order));
|
2025-11-23 16:10:27 +09:00
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('탭 순서가 변경되었습니다');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEditTabFromManage = (tab: typeof customTabs[0]) => {
|
|
|
|
|
if (tab.isDefault) return toast.error('기본 탭은 수정할 수 없습니다');
|
|
|
|
|
setEditingTabId(tab.id);
|
|
|
|
|
setNewTabLabel(tab.label);
|
|
|
|
|
setIsManageTabsDialogOpen(false);
|
|
|
|
|
setIsAddTabDialogOpen(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 현재 섹션의 모든 필드 가져오기 (조건부 필드 참조용)
|
2025-11-23 16:10:27 +09:00
|
|
|
const _getAllFieldsInSection = (sectionId: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!selectedPage) return [];
|
|
|
|
|
const section = selectedPage.sections.find(s => s.id === sectionId);
|
|
|
|
|
return section?.fields || [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 섹션 순서 변경 핸들러 (드래그앤드롭)
|
|
|
|
|
const moveSection = (dragIndex: number, hoverIndex: number) => {
|
|
|
|
|
if (!selectedPage) return;
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
const sections = [...selectedPage.sections];
|
|
|
|
|
const [draggedSection] = sections.splice(dragIndex, 1);
|
|
|
|
|
sections.splice(hoverIndex, 0, draggedSection);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// order 값 재설정
|
|
|
|
|
const updatedSections = sections.map((section, idx) => ({
|
|
|
|
|
...section,
|
|
|
|
|
order: idx + 1
|
|
|
|
|
}));
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 페이지 업데이트
|
|
|
|
|
updateItemPage(selectedPage.id, { sections: updatedSections });
|
2025-11-23 16:10:27 +09:00
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
toast.success('섹션 순서가 변경되었습니다 (저장 필요)');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 순서 변경 핸들러
|
2025-11-23 16:10:27 +09:00
|
|
|
const moveField = (sectionId: number, dragIndex: number, hoverIndex: number) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
if (!selectedPage) return;
|
|
|
|
|
const section = selectedPage.sections.find(s => s.id === sectionId);
|
2025-11-23 16:10:27 +09:00
|
|
|
if (!section || !section.fields) return;
|
2025-11-18 14:17:52 +09:00
|
|
|
const newFields = [...section.fields];
|
|
|
|
|
const [draggedField] = newFields.splice(dragIndex, 1);
|
|
|
|
|
newFields.splice(hoverIndex, 0, draggedField);
|
2025-11-23 16:10:27 +09:00
|
|
|
reorderFields(sectionId, newFields.map(f => f.id));
|
|
|
|
|
// hasUnsavedChanges는 computed value이므로 자동 계산됨
|
2025-11-18 14:17:52 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 전체 데이터 초기화 핸들러
|
|
|
|
|
const _handleResetAllData = () => {
|
|
|
|
|
if (!confirm('⚠️ 경고: 모든 품목기준관리 데이터(계층구조, 섹션, 항목, 속성)를 초기화하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!')) {
|
|
|
|
|
return;
|
2025-11-18 14:17:52 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-23 16:10:27 +09:00
|
|
|
// ItemMasterContext의 모든 데이터 및 캐시 초기화
|
|
|
|
|
resetAllData();
|
|
|
|
|
|
|
|
|
|
// 로컬 상태 초기화 (ItemMasterContext가 관리하지 않는 컴포넌트 로컬 상태)
|
|
|
|
|
setUnitOptions([]);
|
|
|
|
|
setMaterialOptions([]);
|
|
|
|
|
setSurfaceTreatmentOptions([]);
|
|
|
|
|
setCustomAttributeOptions({});
|
|
|
|
|
setAttributeColumns({});
|
|
|
|
|
setBomItems([]);
|
|
|
|
|
|
|
|
|
|
// 탭 상태 초기화 (기본 탭만 남김)
|
|
|
|
|
setCustomTabs([
|
|
|
|
|
{ id: 'hierarchy', label: '계층구조', icon: 'FolderTree', isDefault: true, order: 1 },
|
|
|
|
|
{ id: 'sections', label: '섹션', icon: 'Layers', isDefault: true, order: 2 },
|
|
|
|
|
{ id: 'items', label: '항목', icon: 'ListTree', isDefault: true, order: 3 },
|
|
|
|
|
{ id: 'attributes', label: '속성', icon: 'Settings', isDefault: true, order: 4 }
|
|
|
|
|
]);
|
|
|
|
|
|
2025-11-25 21:07:10 +09:00
|
|
|
setAttributeSubTabs([]);
|
2025-11-23 16:10:27 +09:00
|
|
|
|
|
|
|
|
console.log('🗑️ 모든 품목기준관리 데이터가 초기화되었습니다');
|
|
|
|
|
toast.success('✅ 모든 데이터가 초기화되었습니다!\n계층구조, 섹션, 항목, 속성이 모두 삭제되었습니다.');
|
|
|
|
|
|
|
|
|
|
// 페이지 새로고침하여 완전히 초기화된 상태 반영
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}, 1500);
|
2025-11-18 14:17:52 +09:00
|
|
|
} catch (error) {
|
2025-11-23 16:10:27 +09:00
|
|
|
toast.error('초기화 중 오류가 발생했습니다');
|
|
|
|
|
console.error('Reset error:', error);
|
2025-11-18 14:17:52 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
// 초기 로딩 중 UI
|
|
|
|
|
if (isInitialLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
|
|
|
<LoadingSpinner size="lg" text="데이터를 불러오는 중..." />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 에러 발생 시 UI
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen p-4">
|
|
|
|
|
<ErrorMessage
|
|
|
|
|
message={error}
|
|
|
|
|
onRetry={() => window.location.reload()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
return (
|
|
|
|
|
<PageLayout>
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="품목기준관리"
|
|
|
|
|
description="품목관리에서 사용되는 기준 정보를 설정하고 관리합니다"
|
|
|
|
|
icon={Database}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
|
|
|
<div className="flex items-center gap-2 mb-4">
|
|
|
|
|
<TabsList className="flex-1">
|
|
|
|
|
{customTabs.sort((a, b) => a.order - b.order).map(tab => {
|
|
|
|
|
const Icon = getTabIcon(tab.icon);
|
|
|
|
|
return (
|
|
|
|
|
<TabsTrigger key={tab.id} value={tab.id}>
|
|
|
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
|
|
|
{tab.label}
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TabsList>
|
2025-11-25 21:07:10 +09:00
|
|
|
{/*<Button*/}
|
|
|
|
|
{/* size="sm"*/}
|
|
|
|
|
{/* variant="outline"*/}
|
|
|
|
|
{/* onClick={() => setIsManageTabsDialogOpen(true)}*/}
|
|
|
|
|
{/*>*/}
|
|
|
|
|
{/* <Settings className="h-4 w-4 mr-1" />*/}
|
|
|
|
|
{/* 탭 관리*/}
|
|
|
|
|
{/*</Button>*/}
|
2025-11-23 16:10:27 +09:00
|
|
|
{/* 전체 초기화 버튼 숨김 처리 - 디자인에 없는 기능 */}
|
|
|
|
|
{/* <Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={handleResetAllData}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
|
|
|
전체 초기화
|
|
|
|
|
</Button> */}
|
2025-11-18 14:17:52 +09:00
|
|
|
</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" />
|
2025-11-25 21:07:10 +09:00
|
|
|
항목 관리
|
2025-11-18 14:17:52 +09:00
|
|
|
</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;
|
|
|
|
|
|
|
|
|
|
// 마스터 항목인지 확인
|
2025-11-23 16:10:27 +09:00
|
|
|
const masterField = itemMasterFields.find(f => f.id.toString() === currentTabKey);
|
|
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
// 마스터 항목이면 해당 항목의 속성값들을 표시
|
2025-11-23 16:10:27 +09:00
|
|
|
// Note: default_properties is Record<string, any>, not an array, so this condition will always be false
|
|
|
|
|
// This code block may need refactoring to work with the actual data structure
|
|
|
|
|
if (masterField && masterField.default_properties && Array.isArray(masterField.default_properties)) {
|
2025-11-18 14:17:52 +09:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
<div>
|
2025-11-23 16:10:27 +09:00
|
|
|
<h3 className="font-medium">{masterField.field_name} 속성 목록</h3>
|
2025-11-18 14:17:52 +09:00
|
|
|
<p className="text-sm text-muted-foreground mt-1">
|
2025-11-23 16:10:27 +09:00
|
|
|
항목 탭에서 추가한 "{masterField.field_name}" 항목의 속성값들입니다
|
2025-11-18 14:17:52 +09:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-23 16:10:27 +09:00
|
|
|
|
2025-11-18 14:17:52 +09:00
|
|
|
<div className="space-y-3">
|
2025-11-23 16:10:27 +09:00
|
|
|
{(masterField.default_properties as any[]).map((property: any) => {
|
2025-11-18 14:17:52 +09:00
|
|
|
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">
|
2025-11-23 16:10:27 +09:00
|
|
|
{property.options.map((opt: string, idx: number) => (
|
2025-11-18 14:17:52 +09:00
|
|
|
<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">
|
2025-11-23 16:10:27 +09:00
|
|
|
이 속성들은 <strong>항목 탭</strong>에서 "{masterField.field_name}" 항목을 편집하여 추가/수정/삭제할 수 있습니다.
|
2025-11-18 14:17:52 +09:00
|
|
|
</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">
|
2025-11-23 16:10:27 +09:00
|
|
|
<MasterFieldTab
|
|
|
|
|
itemMasterFields={itemMasterFields}
|
|
|
|
|
setIsMasterFieldDialogOpen={setIsMasterFieldDialogOpen}
|
|
|
|
|
handleEditMasterField={handleEditMasterField}
|
|
|
|
|
handleDeleteMasterField={handleDeleteMasterField}
|
|
|
|
|
hasUnsavedChanges={false}
|
|
|
|
|
pendingChanges={{ masterFields: [] }}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* 섹션관리 탭 */}
|
|
|
|
|
<TabsContent value="sections" className="space-y-4">
|
2025-11-23 16:10:27 +09:00
|
|
|
<SectionsTab
|
|
|
|
|
sectionTemplates={sectionTemplates}
|
|
|
|
|
setIsSectionTemplateDialogOpen={setIsSectionTemplateDialogOpen}
|
|
|
|
|
setCurrentTemplateId={setCurrentTemplateId}
|
|
|
|
|
setIsTemplateFieldDialogOpen={setIsTemplateFieldDialogOpen}
|
|
|
|
|
handleEditSectionTemplate={handleEditSectionTemplate}
|
|
|
|
|
handleDeleteSectionTemplate={handleDeleteSectionTemplate}
|
|
|
|
|
handleEditTemplateField={handleEditTemplateField}
|
|
|
|
|
handleDeleteTemplateField={handleDeleteTemplateField}
|
|
|
|
|
handleAddBOMItemToTemplate={handleAddBOMItemToTemplate}
|
|
|
|
|
handleUpdateBOMItemInTemplate={handleUpdateBOMItemInTemplate}
|
|
|
|
|
handleDeleteBOMItemFromTemplate={handleDeleteBOMItemFromTemplate}
|
|
|
|
|
ITEM_TYPE_OPTIONS={ITEM_TYPE_OPTIONS}
|
|
|
|
|
INPUT_TYPE_OPTIONS={INPUT_TYPE_OPTIONS}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* 계층구조 탭 */}
|
|
|
|
|
<TabsContent value="hierarchy" className="space-y-4">
|
2025-11-23 16:10:27 +09:00
|
|
|
<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={false}
|
|
|
|
|
pendingChanges={{ pages: [], sections: [], fields: [], masterFields: [], attributes: [], sectionTemplates: [] }}
|
|
|
|
|
selectedSectionForField={selectedSectionForField}
|
|
|
|
|
setSelectedSectionForField={setSelectedSectionForField}
|
|
|
|
|
newSectionType={newSectionType}
|
|
|
|
|
setNewSectionType={setNewSectionType}
|
|
|
|
|
updateItemPage={updateItemPage}
|
|
|
|
|
trackChange={() => {}}
|
|
|
|
|
deleteItemPage={handleDeletePageWithTracking}
|
|
|
|
|
duplicatePage={handleDuplicatePage}
|
|
|
|
|
setIsPageDialogOpen={setIsPageDialogOpen}
|
|
|
|
|
setIsSectionDialogOpen={setIsSectionDialogOpen}
|
|
|
|
|
setIsFieldDialogOpen={setIsFieldDialogOpen}
|
|
|
|
|
handleEditSectionTitle={handleEditSectionTitle}
|
|
|
|
|
handleSaveSectionTitle={handleSaveSectionTitle}
|
|
|
|
|
moveSection={moveSection}
|
|
|
|
|
deleteSection={handleDeleteSectionWithTracking}
|
|
|
|
|
updateSection={updateSection}
|
|
|
|
|
deleteField={handleDeleteFieldWithTracking}
|
|
|
|
|
handleEditField={handleEditField}
|
|
|
|
|
moveField={moveField}
|
|
|
|
|
/>
|
|
|
|
|
</TabsContent>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
{/* 사용자 정의 탭들 */}
|
|
|
|
|
{customTabs.filter(tab => !tab.isDefault).map(tab => (
|
|
|
|
|
<TabsContent key={tab.id} value={tab.id}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>{tab.label}</CardTitle>
|
|
|
|
|
<CardDescription>사용자 정의 탭입니다. 여기에 필요한 콘텐츠를 추가할 수 있습니다.</CardDescription>
|
2025-11-18 14:17:52 +09:00
|
|
|
</CardHeader>
|
2025-11-23 16:10:27 +09:00
|
|
|
<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>
|
2025-11-18 14:17:52 +09:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-11-23 16:10:27 +09:00
|
|
|
</TabsContent>
|
|
|
|
|
))}
|
|
|
|
|
</Tabs>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<TabManagementDialogs
|
|
|
|
|
isManageTabsDialogOpen={isManageTabsDialogOpen}
|
|
|
|
|
setIsManageTabsDialogOpen={setIsManageTabsDialogOpen}
|
|
|
|
|
customTabs={customTabs}
|
|
|
|
|
moveTabUp={moveTabUp}
|
|
|
|
|
moveTabDown={moveTabDown}
|
|
|
|
|
handleEditTabFromManage={handleEditTabFromManage}
|
|
|
|
|
handleDeleteTab={handleDeleteTab}
|
|
|
|
|
getTabIcon={getTabIcon}
|
|
|
|
|
setIsAddTabDialogOpen={setIsAddTabDialogOpen}
|
|
|
|
|
isDeleteTabDialogOpen={isDeleteTabDialogOpen}
|
|
|
|
|
setIsDeleteTabDialogOpen={setIsDeleteTabDialogOpen}
|
|
|
|
|
deletingTabId={deletingTabId}
|
|
|
|
|
setDeletingTabId={setDeletingTabId}
|
|
|
|
|
confirmDeleteTab={confirmDeleteTab}
|
|
|
|
|
isAddTabDialogOpen={isAddTabDialogOpen}
|
|
|
|
|
editingTabId={editingTabId}
|
|
|
|
|
setEditingTabId={setEditingTabId}
|
|
|
|
|
newTabLabel={newTabLabel}
|
|
|
|
|
setNewTabLabel={setNewTabLabel}
|
|
|
|
|
handleUpdateTab={handleUpdateTab}
|
|
|
|
|
handleAddTab={handleAddTab}
|
|
|
|
|
isManageAttributeTabsDialogOpen={isManageAttributeTabsDialogOpen}
|
|
|
|
|
setIsManageAttributeTabsDialogOpen={setIsManageAttributeTabsDialogOpen}
|
|
|
|
|
attributeSubTabs={attributeSubTabs}
|
|
|
|
|
moveAttributeTabUp={moveAttributeTabUp}
|
|
|
|
|
moveAttributeTabDown={moveAttributeTabDown}
|
|
|
|
|
handleDeleteAttributeTab={handleDeleteAttributeTab}
|
|
|
|
|
isDeleteAttributeTabDialogOpen={isDeleteAttributeTabDialogOpen}
|
|
|
|
|
setIsDeleteAttributeTabDialogOpen={setIsDeleteAttributeTabDialogOpen}
|
|
|
|
|
deletingAttributeTabId={deletingAttributeTabId}
|
|
|
|
|
setDeletingAttributeTabId={setDeletingAttributeTabId}
|
|
|
|
|
confirmDeleteAttributeTab={confirmDeleteAttributeTab}
|
|
|
|
|
isAddAttributeTabDialogOpen={isAddAttributeTabDialogOpen}
|
|
|
|
|
setIsAddAttributeTabDialogOpen={setIsAddAttributeTabDialogOpen}
|
|
|
|
|
editingAttributeTabId={editingAttributeTabId}
|
|
|
|
|
setEditingAttributeTabId={setEditingAttributeTabId}
|
|
|
|
|
newAttributeTabLabel={newAttributeTabLabel}
|
|
|
|
|
setNewAttributeTabLabel={setNewAttributeTabLabel}
|
|
|
|
|
handleUpdateAttributeTab={handleUpdateAttributeTab}
|
|
|
|
|
handleAddAttributeTab={handleAddAttributeTab}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<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}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<ColumnManageDialog
|
|
|
|
|
isOpen={isColumnManageDialogOpen}
|
|
|
|
|
setIsOpen={setIsColumnManageDialogOpen}
|
|
|
|
|
managingColumnType={managingColumnType}
|
|
|
|
|
attributeSubTabs={attributeSubTabs}
|
|
|
|
|
attributeColumns={attributeColumns}
|
|
|
|
|
setAttributeColumns={setAttributeColumns}
|
|
|
|
|
newColumnName={newColumnName}
|
|
|
|
|
setNewColumnName={setNewColumnName}
|
|
|
|
|
newColumnKey={newColumnKey}
|
|
|
|
|
setNewColumnKey={setNewColumnKey}
|
|
|
|
|
newColumnType={newColumnType}
|
|
|
|
|
setNewColumnType={setNewColumnType}
|
|
|
|
|
newColumnRequired={newColumnRequired}
|
|
|
|
|
setNewColumnRequired={setNewColumnRequired}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<PathEditDialog
|
|
|
|
|
editingPathPageId={editingPathPageId}
|
|
|
|
|
setEditingPathPageId={setEditingPathPageId}
|
|
|
|
|
editingAbsolutePath={editingAbsolutePath}
|
|
|
|
|
setEditingAbsolutePath={setEditingAbsolutePath}
|
|
|
|
|
updateItemPage={updateItemPage}
|
|
|
|
|
trackChange={() => {}}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<PageDialog
|
|
|
|
|
isPageDialogOpen={isPageDialogOpen}
|
|
|
|
|
setIsPageDialogOpen={setIsPageDialogOpen}
|
|
|
|
|
newPageName={newPageName}
|
|
|
|
|
setNewPageName={setNewPageName}
|
|
|
|
|
newPageItemType={newPageItemType}
|
|
|
|
|
setNewPageItemType={setNewPageItemType}
|
|
|
|
|
handleAddPage={handleAddPage}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<SectionDialog
|
|
|
|
|
isSectionDialogOpen={isSectionDialogOpen}
|
|
|
|
|
setIsSectionDialogOpen={setIsSectionDialogOpen}
|
|
|
|
|
newSectionType={newSectionType}
|
|
|
|
|
setNewSectionType={setNewSectionType}
|
|
|
|
|
newSectionTitle={newSectionTitle}
|
|
|
|
|
setNewSectionTitle={setNewSectionTitle}
|
|
|
|
|
newSectionDescription={newSectionDescription}
|
|
|
|
|
setNewSectionDescription={setNewSectionDescription}
|
|
|
|
|
handleAddSection={handleAddSection}
|
2025-11-25 21:07:10 +09:00
|
|
|
sectionInputMode={sectionInputMode}
|
|
|
|
|
setSectionInputMode={setSectionInputMode}
|
|
|
|
|
sectionTemplates={sectionTemplates}
|
|
|
|
|
selectedTemplateId={selectedSectionTemplateId}
|
|
|
|
|
setSelectedTemplateId={setSelectedSectionTemplateId}
|
|
|
|
|
handleLinkTemplate={handleLinkTemplate}
|
2025-11-23 16:10:27 +09:00
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
{/* 항목 추가/수정 다이얼로그 - 데스크톱 */}
|
|
|
|
|
{!isMobile && (
|
2025-11-23 16:10:27 +09:00
|
|
|
<FieldDialog
|
|
|
|
|
isOpen={isFieldDialogOpen}
|
|
|
|
|
onOpenChange={setIsFieldDialogOpen}
|
|
|
|
|
editingFieldId={editingFieldId}
|
|
|
|
|
setEditingFieldId={setEditingFieldId}
|
|
|
|
|
fieldInputMode={fieldInputMode}
|
|
|
|
|
setFieldInputMode={setFieldInputMode}
|
|
|
|
|
showMasterFieldList={showMasterFieldList}
|
|
|
|
|
setShowMasterFieldList={setShowMasterFieldList}
|
|
|
|
|
selectedMasterFieldId={selectedMasterFieldId}
|
|
|
|
|
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
|
|
|
|
textboxColumns={textboxColumns}
|
|
|
|
|
setTextboxColumns={setTextboxColumns}
|
|
|
|
|
newFieldConditionEnabled={newFieldConditionEnabled}
|
|
|
|
|
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
|
|
|
|
newFieldConditionTargetType={newFieldConditionTargetType}
|
|
|
|
|
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
|
|
|
|
newFieldConditionFields={newFieldConditionFields}
|
|
|
|
|
setNewFieldConditionFields={setNewFieldConditionFields}
|
|
|
|
|
newFieldConditionSections={newFieldConditionSections}
|
|
|
|
|
setNewFieldConditionSections={setNewFieldConditionSections}
|
|
|
|
|
tempConditionValue={tempConditionValue}
|
|
|
|
|
setTempConditionValue={setTempConditionValue}
|
|
|
|
|
newFieldName={newFieldName}
|
|
|
|
|
setNewFieldName={setNewFieldName}
|
|
|
|
|
newFieldKey={newFieldKey}
|
|
|
|
|
setNewFieldKey={setNewFieldKey}
|
|
|
|
|
newFieldInputType={newFieldInputType}
|
|
|
|
|
setNewFieldInputType={setNewFieldInputType}
|
|
|
|
|
newFieldRequired={newFieldRequired}
|
|
|
|
|
setNewFieldRequired={setNewFieldRequired}
|
|
|
|
|
newFieldDescription={newFieldDescription}
|
|
|
|
|
setNewFieldDescription={setNewFieldDescription}
|
|
|
|
|
newFieldOptions={newFieldOptions}
|
|
|
|
|
setNewFieldOptions={setNewFieldOptions}
|
|
|
|
|
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
|
|
|
|
selectedPage={selectedPage || null}
|
|
|
|
|
itemMasterFields={itemMasterFields}
|
|
|
|
|
handleAddField={handleAddField}
|
|
|
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
|
|
|
setEditingColumnId={setEditingColumnId}
|
|
|
|
|
setColumnName={setColumnName}
|
|
|
|
|
setColumnKey={setColumnKey}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 항목 추가/수정 다이얼로그 - 모바일 (바텀시트) */}
|
|
|
|
|
{isMobile && (
|
2025-11-23 16:10:27 +09:00
|
|
|
<FieldDrawer
|
|
|
|
|
isOpen={isFieldDialogOpen}
|
|
|
|
|
onOpenChange={setIsFieldDialogOpen}
|
|
|
|
|
editingFieldId={editingFieldId}
|
|
|
|
|
setEditingFieldId={setEditingFieldId}
|
|
|
|
|
fieldInputMode={fieldInputMode}
|
|
|
|
|
setFieldInputMode={setFieldInputMode}
|
|
|
|
|
showMasterFieldList={showMasterFieldList}
|
|
|
|
|
setShowMasterFieldList={setShowMasterFieldList}
|
|
|
|
|
selectedMasterFieldId={selectedMasterFieldId}
|
|
|
|
|
setSelectedMasterFieldId={setSelectedMasterFieldId}
|
|
|
|
|
textboxColumns={textboxColumns}
|
|
|
|
|
setTextboxColumns={setTextboxColumns}
|
|
|
|
|
newFieldConditionEnabled={newFieldConditionEnabled}
|
|
|
|
|
setNewFieldConditionEnabled={setNewFieldConditionEnabled}
|
|
|
|
|
newFieldConditionTargetType={newFieldConditionTargetType}
|
|
|
|
|
setNewFieldConditionTargetType={setNewFieldConditionTargetType}
|
|
|
|
|
newFieldConditionFields={newFieldConditionFields}
|
|
|
|
|
setNewFieldConditionFields={setNewFieldConditionFields}
|
|
|
|
|
newFieldConditionSections={newFieldConditionSections}
|
|
|
|
|
setNewFieldConditionSections={setNewFieldConditionSections}
|
|
|
|
|
tempConditionValue={tempConditionValue}
|
|
|
|
|
setTempConditionValue={setTempConditionValue}
|
|
|
|
|
newFieldName={newFieldName}
|
|
|
|
|
setNewFieldName={setNewFieldName}
|
|
|
|
|
newFieldKey={newFieldKey}
|
|
|
|
|
setNewFieldKey={setNewFieldKey}
|
|
|
|
|
newFieldInputType={newFieldInputType}
|
|
|
|
|
setNewFieldInputType={setNewFieldInputType}
|
|
|
|
|
newFieldRequired={newFieldRequired}
|
|
|
|
|
setNewFieldRequired={setNewFieldRequired}
|
|
|
|
|
newFieldDescription={newFieldDescription}
|
|
|
|
|
setNewFieldDescription={setNewFieldDescription}
|
|
|
|
|
newFieldOptions={newFieldOptions}
|
|
|
|
|
setNewFieldOptions={setNewFieldOptions}
|
|
|
|
|
selectedSectionForField={selectedPage?.sections.find(s => s.id === selectedSectionForField) || null}
|
|
|
|
|
selectedPage={selectedPage || null}
|
|
|
|
|
itemMasterFields={itemMasterFields}
|
|
|
|
|
handleAddField={handleAddField}
|
|
|
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
|
|
|
setEditingColumnId={setEditingColumnId}
|
|
|
|
|
setColumnName={setColumnName}
|
|
|
|
|
setColumnKey={setColumnKey}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-11-18 14:17:52 +09:00
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
{/* 텍스트박스 컬럼 추가/수정 다이얼로그 */}
|
|
|
|
|
<ColumnDialog
|
|
|
|
|
isColumnDialogOpen={isColumnDialogOpen}
|
|
|
|
|
setIsColumnDialogOpen={setIsColumnDialogOpen}
|
|
|
|
|
editingColumnId={editingColumnId}
|
|
|
|
|
setEditingColumnId={setEditingColumnId}
|
|
|
|
|
columnName={columnName}
|
|
|
|
|
setColumnName={setColumnName}
|
|
|
|
|
columnKey={columnKey}
|
|
|
|
|
setColumnKey={setColumnKey}
|
|
|
|
|
textboxColumns={textboxColumns}
|
|
|
|
|
setTextboxColumns={setTextboxColumns}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<MasterFieldDialog
|
|
|
|
|
isMasterFieldDialogOpen={isMasterFieldDialogOpen}
|
|
|
|
|
setIsMasterFieldDialogOpen={setIsMasterFieldDialogOpen}
|
|
|
|
|
editingMasterFieldId={editingMasterFieldId}
|
|
|
|
|
setEditingMasterFieldId={setEditingMasterFieldId}
|
|
|
|
|
newMasterFieldName={newMasterFieldName}
|
|
|
|
|
setNewMasterFieldName={setNewMasterFieldName}
|
|
|
|
|
newMasterFieldKey={newMasterFieldKey}
|
|
|
|
|
setNewMasterFieldKey={setNewMasterFieldKey}
|
|
|
|
|
newMasterFieldInputType={newMasterFieldInputType}
|
|
|
|
|
setNewMasterFieldInputType={setNewMasterFieldInputType}
|
|
|
|
|
newMasterFieldRequired={newMasterFieldRequired}
|
|
|
|
|
setNewMasterFieldRequired={setNewMasterFieldRequired}
|
|
|
|
|
newMasterFieldCategory={newMasterFieldCategory}
|
|
|
|
|
setNewMasterFieldCategory={setNewMasterFieldCategory}
|
|
|
|
|
newMasterFieldDescription={newMasterFieldDescription}
|
|
|
|
|
setNewMasterFieldDescription={setNewMasterFieldDescription}
|
|
|
|
|
newMasterFieldOptions={newMasterFieldOptions}
|
|
|
|
|
setNewMasterFieldOptions={setNewMasterFieldOptions}
|
|
|
|
|
newMasterFieldAttributeType={newMasterFieldAttributeType}
|
|
|
|
|
setNewMasterFieldAttributeType={setNewMasterFieldAttributeType}
|
|
|
|
|
newMasterFieldMultiColumn={newMasterFieldMultiColumn}
|
|
|
|
|
setNewMasterFieldMultiColumn={setNewMasterFieldMultiColumn}
|
|
|
|
|
newMasterFieldColumnCount={newMasterFieldColumnCount}
|
|
|
|
|
setNewMasterFieldColumnCount={setNewMasterFieldColumnCount}
|
|
|
|
|
newMasterFieldColumnNames={newMasterFieldColumnNames}
|
|
|
|
|
setNewMasterFieldColumnNames={setNewMasterFieldColumnNames}
|
|
|
|
|
handleUpdateMasterField={handleUpdateMasterField}
|
|
|
|
|
handleAddMasterField={handleAddMasterField}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<SectionTemplateDialog
|
|
|
|
|
isSectionTemplateDialogOpen={isSectionTemplateDialogOpen}
|
|
|
|
|
setIsSectionTemplateDialogOpen={setIsSectionTemplateDialogOpen}
|
|
|
|
|
editingSectionTemplateId={editingSectionTemplateId}
|
|
|
|
|
setEditingSectionTemplateId={setEditingSectionTemplateId}
|
|
|
|
|
newSectionTemplateTitle={newSectionTemplateTitle}
|
|
|
|
|
setNewSectionTemplateTitle={setNewSectionTemplateTitle}
|
|
|
|
|
newSectionTemplateDescription={newSectionTemplateDescription}
|
|
|
|
|
setNewSectionTemplateDescription={setNewSectionTemplateDescription}
|
|
|
|
|
newSectionTemplateCategory={newSectionTemplateCategory}
|
|
|
|
|
setNewSectionTemplateCategory={setNewSectionTemplateCategory}
|
|
|
|
|
newSectionTemplateType={newSectionTemplateType}
|
|
|
|
|
setNewSectionTemplateType={setNewSectionTemplateType}
|
|
|
|
|
handleUpdateSectionTemplate={handleUpdateSectionTemplate}
|
|
|
|
|
handleAddSectionTemplate={handleAddSectionTemplate}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
|
|
|
|
|
|
2025-11-23 16:10:27 +09:00
|
|
|
<TemplateFieldDialog
|
|
|
|
|
isTemplateFieldDialogOpen={isTemplateFieldDialogOpen}
|
|
|
|
|
setIsTemplateFieldDialogOpen={setIsTemplateFieldDialogOpen}
|
|
|
|
|
editingTemplateFieldId={editingTemplateFieldId}
|
|
|
|
|
setEditingTemplateFieldId={setEditingTemplateFieldId}
|
|
|
|
|
templateFieldName={templateFieldName}
|
|
|
|
|
setTemplateFieldName={setTemplateFieldName}
|
|
|
|
|
templateFieldKey={templateFieldKey}
|
|
|
|
|
setTemplateFieldKey={setTemplateFieldKey}
|
|
|
|
|
templateFieldInputType={templateFieldInputType}
|
|
|
|
|
setTemplateFieldInputType={setTemplateFieldInputType}
|
|
|
|
|
templateFieldRequired={templateFieldRequired}
|
|
|
|
|
setTemplateFieldRequired={setTemplateFieldRequired}
|
|
|
|
|
templateFieldOptions={templateFieldOptions}
|
|
|
|
|
setTemplateFieldOptions={setTemplateFieldOptions}
|
|
|
|
|
templateFieldDescription={templateFieldDescription}
|
|
|
|
|
setTemplateFieldDescription={setTemplateFieldDescription}
|
|
|
|
|
templateFieldMultiColumn={templateFieldMultiColumn}
|
|
|
|
|
setTemplateFieldMultiColumn={setTemplateFieldMultiColumn}
|
|
|
|
|
templateFieldColumnCount={templateFieldColumnCount}
|
|
|
|
|
setTemplateFieldColumnCount={setTemplateFieldColumnCount}
|
|
|
|
|
templateFieldColumnNames={templateFieldColumnNames}
|
|
|
|
|
setTemplateFieldColumnNames={setTemplateFieldColumnNames}
|
|
|
|
|
handleAddTemplateField={handleAddTemplateField}
|
2025-11-25 21:07:10 +09:00
|
|
|
// 마스터 항목 관련 props
|
|
|
|
|
itemMasterFields={itemMasterFields}
|
|
|
|
|
templateFieldInputMode={templateFieldInputMode}
|
|
|
|
|
setTemplateFieldInputMode={setTemplateFieldInputMode}
|
|
|
|
|
showMasterFieldList={templateFieldShowMasterFieldList}
|
|
|
|
|
setShowMasterFieldList={setTemplateFieldShowMasterFieldList}
|
|
|
|
|
selectedMasterFieldId={templateFieldSelectedMasterFieldId}
|
|
|
|
|
setSelectedMasterFieldId={setTemplateFieldSelectedMasterFieldId}
|
2025-11-23 16:10:27 +09:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<LoadTemplateDialog
|
|
|
|
|
isLoadTemplateDialogOpen={isLoadTemplateDialogOpen}
|
|
|
|
|
setIsLoadTemplateDialogOpen={setIsLoadTemplateDialogOpen}
|
|
|
|
|
sectionTemplates={sectionTemplates}
|
|
|
|
|
selectedTemplateId={selectedTemplateId}
|
|
|
|
|
setSelectedTemplateId={setSelectedTemplateId}
|
|
|
|
|
handleLoadTemplate={handleLoadTemplate}
|
|
|
|
|
/>
|
2025-11-18 14:17:52 +09:00
|
|
|
</PageLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|