From 3b52847d89788d30255ab1314304c93832c79cff Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Tue, 6 Jan 2026 20:13:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20422=20ValidationException=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20AlertDialog=20=ED=8C=9D=EC=97=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorAlertContext 생성 (전역 에러 알림 상태 관리) - useFieldManagement, useMasterFieldManagement, useTemplateManagement에 적용 - 중복 이름, 예약어 사용 시 디자인된 AlertDialog 팝업 표시 - toast 대신 모달 위에 표시되는 팝업으로 변경 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/HierarchyTab.tsx | 10 + .../items/ItemMasterDataManagement.tsx | 13 + .../components/ErrorAlertDialog.tsx | 51 ++ .../contexts/ErrorAlertContext.tsx | 93 ++++ .../contexts/index.ts | 1 + .../hooks/useErrorAlert.ts | 48 ++ .../hooks/useFieldManagement.ts | 23 +- .../hooks/useMasterFieldManagement.ts | 85 +++- .../hooks/useTemplateManagement.ts | 23 +- src/contexts/ItemMasterContext.tsx | 453 +++--------------- src/stores/item-master/useItemMasterStore.ts | 46 +- src/types/item-master.types.ts | 392 +++++++++++++++ 12 files changed, 821 insertions(+), 417 deletions(-) create mode 100644 src/components/items/ItemMasterDataManagement/components/ErrorAlertDialog.tsx create mode 100644 src/components/items/ItemMasterDataManagement/contexts/ErrorAlertContext.tsx create mode 100644 src/components/items/ItemMasterDataManagement/contexts/index.ts create mode 100644 src/components/items/ItemMasterDataManagement/hooks/useErrorAlert.ts create mode 100644 src/types/item-master.types.ts diff --git a/src/app/[locale]/(protected)/items-management-test/components/HierarchyTab.tsx b/src/app/[locale]/(protected)/items-management-test/components/HierarchyTab.tsx index c66e8831..73f84af9 100644 --- a/src/app/[locale]/(protected)/items-management-test/components/HierarchyTab.tsx +++ b/src/app/[locale]/(protected)/items-management-test/components/HierarchyTab.tsx @@ -150,6 +150,16 @@ export function HierarchyTab() { .map((id) => entities.sections[id]) .filter(Boolean) || []; + // 🔍 DEBUG: 상태 변경 추적 + console.log('[HierarchyTab] 렌더링:', { + selectedPageId, + selectedPageName: selectedPage?.page_name, + sectionIds: selectedPage?.sectionIds, + pageSectionsCount: pageSections.length, + entitiesPagesCount: Object.keys(entities.pages).length, + entitiesSectionsCount: Object.keys(entities.sections).length, + }); + // 섹션 접힘 토글 const toggleSection = (sectionId: number) => { setCollapsedSections((prev) => ({ diff --git a/src/components/items/ItemMasterDataManagement.tsx b/src/components/items/ItemMasterDataManagement.tsx index b984357c..06dc106e 100644 --- a/src/components/items/ItemMasterDataManagement.tsx +++ b/src/components/items/ItemMasterDataManagement.tsx @@ -51,6 +51,9 @@ import { useDeleteManagement, } from './ItemMasterDataManagement/hooks'; +// 에러 알림 Context +import { ErrorAlertProvider } from './ItemMasterDataManagement/contexts'; + const ITEM_TYPE_OPTIONS = [ { value: 'FG', label: '제품 (FG)' }, { value: 'PT', label: '부품 (PT)' }, @@ -68,7 +71,17 @@ const INPUT_TYPE_OPTIONS = [ { value: 'textarea', label: '텍스트영역' } ]; +// Wrapper 컴포넌트: ErrorAlertProvider를 먼저 제공 export function ItemMasterDataManagement() { + return ( + + + + ); +} + +// 실제 로직을 담는 내부 컴포넌트 +function ItemMasterDataManagementContent() { const { itemPages, loadItemPages: _loadItemPages, diff --git a/src/components/items/ItemMasterDataManagement/components/ErrorAlertDialog.tsx b/src/components/items/ItemMasterDataManagement/components/ErrorAlertDialog.tsx new file mode 100644 index 00000000..8c50f38c --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/components/ErrorAlertDialog.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { AlertCircle } from 'lucide-react'; + +interface ErrorAlertDialogProps { + open: boolean; + onClose: () => void; + title?: string; + message: string; +} + +/** + * 에러 알림 다이얼로그 컴포넌트 + * 422 ValidationException 등의 에러 메시지를 표시 + */ +export function ErrorAlertDialog({ + open, + onClose, + title = '오류', + message, +}: ErrorAlertDialogProps) { + return ( + !isOpen && onClose()}> + + + + + {title} + + + {message} + + + + + 확인 + + + + + ); +} \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/contexts/ErrorAlertContext.tsx b/src/components/items/ItemMasterDataManagement/contexts/ErrorAlertContext.tsx new file mode 100644 index 00000000..3505b26b --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/contexts/ErrorAlertContext.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { AlertCircle } from 'lucide-react'; + +interface ErrorAlertState { + open: boolean; + title: string; + message: string; +} + +interface ErrorAlertContextType { + showErrorAlert: (message: string, title?: string) => void; +} + +const ErrorAlertContext = createContext(null); + +/** + * 에러 알림 Context 사용 훅 + */ +export function useErrorAlert() { + const context = useContext(ErrorAlertContext); + if (!context) { + throw new Error('useErrorAlert must be used within ErrorAlertProvider'); + } + return context; +} + +interface ErrorAlertProviderProps { + children: ReactNode; +} + +/** + * 에러 알림 Provider + * ItemMasterDataManagement 컴포넌트에서 사용 + */ +export function ErrorAlertProvider({ children }: ErrorAlertProviderProps) { + const [errorAlert, setErrorAlert] = useState({ + open: false, + title: '오류', + message: '', + }); + + const showErrorAlert = useCallback((message: string, title: string = '오류') => { + setErrorAlert({ + open: true, + title, + message, + }); + }, []); + + const closeErrorAlert = useCallback(() => { + setErrorAlert(prev => ({ + ...prev, + open: false, + })); + }, []); + + return ( + + {children} + + {/* 에러 알림 다이얼로그 */} + !isOpen && closeErrorAlert()}> + + + + + {errorAlert.title} + + + {errorAlert.message} + + + + + 확인 + + + + + + ); +} diff --git a/src/components/items/ItemMasterDataManagement/contexts/index.ts b/src/components/items/ItemMasterDataManagement/contexts/index.ts new file mode 100644 index 00000000..1f705b8f --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/contexts/index.ts @@ -0,0 +1 @@ +export { ErrorAlertProvider, useErrorAlert } from './ErrorAlertContext'; \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/useErrorAlert.ts b/src/components/items/ItemMasterDataManagement/hooks/useErrorAlert.ts new file mode 100644 index 00000000..179a921d --- /dev/null +++ b/src/components/items/ItemMasterDataManagement/hooks/useErrorAlert.ts @@ -0,0 +1,48 @@ +'use client'; + +import { useState, useCallback } from 'react'; + +export interface ErrorAlertState { + open: boolean; + title: string; + message: string; +} + +export interface UseErrorAlertReturn { + errorAlert: ErrorAlertState; + showErrorAlert: (message: string, title?: string) => void; + closeErrorAlert: () => void; +} + +/** + * 에러 알림 다이얼로그 상태 관리 훅 + * AlertDialog로 에러 메시지를 표시할 때 사용 + */ +export function useErrorAlert(): UseErrorAlertReturn { + const [errorAlert, setErrorAlert] = useState({ + open: false, + title: '오류', + message: '', + }); + + const showErrorAlert = useCallback((message: string, title: string = '오류') => { + setErrorAlert({ + open: true, + title, + message, + }); + }, []); + + const closeErrorAlert = useCallback(() => { + setErrorAlert(prev => ({ + ...prev, + open: false, + })); + }, []); + + return { + errorAlert, + showErrorAlert, + closeErrorAlert, + }; +} \ No newline at end of file diff --git a/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts index 53aaab9f..36e700f3 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useFieldManagement.ts @@ -3,9 +3,11 @@ import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { useErrorAlert } from '../contexts'; import type { ItemPage, ItemField, ItemMasterField, FieldDisplayCondition } from '@/contexts/ItemMasterContext'; import { type ConditionalFieldConfig } from '../components/ConditionalDisplayUI'; import { fieldService } from '../services'; +import { ApiError } from '@/lib/api/error-handler'; export interface UseFieldManagementReturn { // 다이얼로그 상태 @@ -79,6 +81,9 @@ export function useFieldManagement(): UseFieldManagementReturn { updateItemMasterField, } = useItemMaster(); + // 에러 알림 (AlertDialog로 표시) + const { showErrorAlert } = useErrorAlert(); + // 다이얼로그 상태 const [isFieldDialogOpen, setIsFieldDialogOpen] = useState(false); const [selectedSectionForField, setSelectedSectionForField] = useState(null); @@ -238,7 +243,23 @@ export function useFieldManagement(): UseFieldManagementReturn { resetFieldForm(); } catch (error) { console.error('필드 처리 실패:', error); - toast.error('항목 처리에 실패했습니다'); + + // 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) + if (error instanceof ApiError) { + console.log('🔍 ApiError.errors:', error.errors); // 디버깅용 + + // errors 객체에서 첫 번째 에러 메시지 추출 → AlertDialog로 표시 + if (error.errors && Object.keys(error.errors).length > 0) { + const firstKey = Object.keys(error.errors)[0]; + const firstError = error.errors[firstKey]; + const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError; + showErrorAlert(errorMessage, '항목 저장 실패'); + } else { + showErrorAlert(error.message, '항목 저장 실패'); + } + } else { + showErrorAlert('항목 처리에 실패했습니다', '오류'); + } } }; diff --git a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts index 256ff94c..5fff123d 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useMasterFieldManagement.ts @@ -3,8 +3,10 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { useErrorAlert } from '../contexts'; import type { ItemMasterField } from '@/contexts/ItemMasterContext'; import { masterFieldService } from '../services'; +import { ApiError } from '@/lib/api/error-handler'; /** * @deprecated 2025-11-27: item_fields로 통합됨. @@ -44,10 +46,10 @@ export interface UseMasterFieldManagementReturn { setNewMasterFieldColumnNames: React.Dispatch>; // 핸들러 - handleAddMasterField: () => void; + handleAddMasterField: () => Promise; handleEditMasterField: (field: ItemMasterField) => void; - handleUpdateMasterField: () => void; - handleDeleteMasterField: (id: number) => void; + handleUpdateMasterField: () => Promise; + handleDeleteMasterField: (id: number) => Promise; resetMasterFieldForm: () => void; } @@ -59,6 +61,9 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { deleteItemMasterField, } = useItemMaster(); + // 에러 알림 (AlertDialog로 표시) + const { showErrorAlert } = useErrorAlert(); + // 다이얼로그 상태 const [isMasterFieldDialogOpen, setIsMasterFieldDialogOpen] = useState(false); const [editingMasterFieldId, setEditingMasterFieldId] = useState(null); @@ -77,7 +82,7 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState(['컬럼1', '컬럼2']); // 마스터 항목 추가 - const handleAddMasterField = () => { + const handleAddMasterField = async () => { if (!newMasterFieldName.trim() || !newMasterFieldKey.trim()) { toast.error('항목명과 필드 키를 입력해주세요'); return; @@ -106,9 +111,30 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { }, }; - addItemMasterField(newMasterFieldData as any); - resetMasterFieldForm(); - toast.success('항목이 추가되었습니다'); + try { + await addItemMasterField(newMasterFieldData as any); + resetMasterFieldForm(); + toast.success('항목이 추가되었습니다'); + } catch (error) { + console.error('항목 추가 실패:', error); + + // 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어) → AlertDialog로 표시 + if (error instanceof ApiError) { + console.log('🔍 ApiError.errors:', error.errors); // 디버깅용 + + // errors 객체에서 첫 번째 에러 메시지 추출 + if (error.errors && Object.keys(error.errors).length > 0) { + const firstKey = Object.keys(error.errors)[0]; + const firstError = error.errors[firstKey]; + const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError; + showErrorAlert(errorMessage, '항목 추가 실패'); + } else { + showErrorAlert(error.message, '항목 추가 실패'); + } + } else { + showErrorAlert('항목 추가에 실패했습니다', '오류'); + } + } }; // 마스터 항목 수정 시작 @@ -134,7 +160,7 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { }; // 마스터 항목 업데이트 - const handleUpdateMasterField = () => { + const handleUpdateMasterField = async () => { if (!editingMasterFieldId || !newMasterFieldName.trim() || !newMasterFieldKey.trim()) { toast.error('항목명과 필드 키를 입력해주세요'); return; @@ -159,16 +185,47 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn { }, }; - updateItemMasterField(editingMasterFieldId, updateData); - resetMasterFieldForm(); - toast.success('항목이 수정되었습니다'); + try { + await updateItemMasterField(editingMasterFieldId, updateData); + resetMasterFieldForm(); + toast.success('항목이 수정되었습니다'); + } catch (error) { + console.error('항목 수정 실패:', error); + + // 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시 + if (error instanceof ApiError) { + console.log('🔍 ApiError.errors:', error.errors); // 디버깅용 + + // errors 객체에서 첫 번째 에러 메시지 추출 + if (error.errors && Object.keys(error.errors).length > 0) { + const firstKey = Object.keys(error.errors)[0]; + const firstError = error.errors[firstKey]; + const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError; + showErrorAlert(errorMessage, '항목 수정 실패'); + } else { + showErrorAlert(error.message, '항목 수정 실패'); + } + } else { + showErrorAlert('항목 수정에 실패했습니다', '오류'); + } + } }; // 항목 삭제 (2025-11-27: 마스터 항목 → 항목으로 통합) - const handleDeleteMasterField = (id: number) => { + const handleDeleteMasterField = async (id: number) => { if (confirm('이 항목을 삭제하시겠습니까?\n(섹션에서 사용 중인 경우 연결도 함께 해제됩니다)')) { - deleteItemMasterField(id); - toast.success('항목이 삭제되었습니다'); + try { + await deleteItemMasterField(id); + toast.success('항목이 삭제되었습니다'); + } catch (error) { + console.error('항목 삭제 실패:', error); + + if (error instanceof ApiError) { + toast.error(error.message); + } else { + toast.error('항목 삭제에 실패했습니다'); + } + } } }; diff --git a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts index c083e2c5..b2de6f90 100644 --- a/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts +++ b/src/components/items/ItemMasterDataManagement/hooks/useTemplateManagement.ts @@ -3,8 +3,10 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { useItemMaster } from '@/contexts/ItemMasterContext'; +import { useErrorAlert } from '../contexts'; import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext'; import { templateService } from '../services'; +import { ApiError } from '@/lib/api/error-handler'; export interface UseTemplateManagementReturn { // 섹션 템플릿 다이얼로그 상태 @@ -112,6 +114,9 @@ export function useTemplateManagement(): UseTemplateManagementReturn { deleteBOMItem, } = useItemMaster(); + // 에러 알림 (AlertDialog로 표시) + const { showErrorAlert } = useErrorAlert(); + // 섹션 템플릿 다이얼로그 상태 const [isSectionTemplateDialogOpen, setIsSectionTemplateDialogOpen] = useState(false); const [editingSectionTemplateId, setEditingSectionTemplateId] = useState(null); @@ -348,7 +353,23 @@ export function useTemplateManagement(): UseTemplateManagementReturn { resetTemplateFieldForm(); } catch (error) { console.error('항목 처리 실패:', error); - toast.error('항목 처리에 실패했습니다'); + + // 422 ValidationException 상세 메시지 처리 (field_key 중복/예약어, field_name 중복 등) → AlertDialog로 표시 + if (error instanceof ApiError) { + console.log('🔍 ApiError.errors:', error.errors); // 디버깅용 + + // errors 객체에서 첫 번째 에러 메시지 추출 + if (error.errors && Object.keys(error.errors).length > 0) { + const firstKey = Object.keys(error.errors)[0]; + const firstError = error.errors[firstKey]; + const errorMessage = Array.isArray(firstError) ? firstError[0] : firstError; + showErrorAlert(errorMessage, '항목 저장 실패'); + } else { + showErrorAlert(error.message, '항목 저장 실패'); + } + } else { + showErrorAlert('항목 처리에 실패했습니다', '오류'); + } } }; diff --git a/src/contexts/ItemMasterContext.tsx b/src/contexts/ItemMasterContext.tsx index 54b93912..60053a98 100644 --- a/src/contexts/ItemMasterContext.tsx +++ b/src/contexts/ItemMasterContext.tsx @@ -22,392 +22,56 @@ import type { FieldUsageResponse, } from '@/types/item-master-api'; -// ===== Type Definitions ===== +// 타입 정의는 별도 파일에서 import +export type { + BendingDetail, + BOMLine, + SpecificationMaster, + MaterialItemName, + ItemRevision, + ItemMaster, + ItemCategory, + ItemUnit, + ItemMaterial, + SurfaceTreatment, + PartTypeOption, + PartUsageOption, + GuideRailOption, + ItemFieldProperty, + ItemMasterField, + FieldDisplayCondition, + ItemField, + BOMItem, + ItemSection, + ItemPage, + TemplateField, + SectionTemplate, +} from '@/types/item-master.types'; -// 전개도 상세 정보 -export interface BendingDetail { - id: string; - no: number; // 번호 - input: number; // 입력 - elongation: number; // 연신율 (기본값 -1) - calculated: number; // 연신율 계산 후 - sum: number; // 합계 - shaded: boolean; // 음영 여부 - aAngle?: number; // A각 -} - -// 부품구성표(BOM, Bill of Materials) - 자재 명세서 -export interface BOMLine { - id: string; - childItemCode: string; // 구성 품목 코드 - childItemName: string; // 구성 품목명 - quantity: number; // 기준 수량 - unit: string; // 단위 - unitPrice?: number; // 단가 - quantityFormula?: string; // 수량 계산식 (예: "W * 2", "H + 100") - note?: string; // 비고 - // 절곡품 관련 (하위 절곡 부품용) - isBending?: boolean; - bendingDiagram?: string; // 전개도 이미지 URL - bendingDetails?: BendingDetail[]; // 전개도 상세 데이터 -} - -// 규격 마스터 (원자재/부자재용) -export interface SpecificationMaster { - id: string; - specificationCode: string; // 규격 코드 (예: 1.6T x 1219 x 2438) - itemType: 'RM' | 'SM'; // 원자재 | 부자재 - itemName?: string; // 품목명 (예: SPHC-SD, SPCC-SD) - 품목명별 규격 필터링용 - fieldCount: '1' | '2' | '3'; // 너비 입력 개수 - thickness: string; // 두께 - widthA: string; // 너비A - widthB?: string; // 너비B - widthC?: string; // 너비C - length: string; // 길이 - description?: string; // 설명 - isActive: boolean; // 활성 여부 - createdAt?: string; - updatedAt?: string; -} - -// 원자재/부자재 품목명 마스터 -export interface MaterialItemName { - id: string; - itemType: 'RM' | 'SM'; // 원자재 | 부자재 - itemName: string; // 품목명 (예: "SPHC-SD", "STS430") - category?: string; // 분류 (예: "냉연", "열연", "스테인리스") - description?: string; // 설명 - isActive: boolean; // 활성 여부 - createdAt: string; - updatedAt?: string; -} - -// 품목 수정 이력 -export interface ItemRevision { - revisionNumber: number; // 수정 차수 (1차, 2차, 3차...) - revisionDate: string; // 수정일 - revisionBy: string; // 수정자 - revisionReason?: string; // 수정 사유 - previousData: any; // 이전 버전의 전체 데이터 -} - -// 품목 마스터 -export interface ItemMaster { - id: string; - itemCode: string; - itemName: string; - itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 제품, 부품, 부자재, 원자재, 소모품 - productCategory?: 'SCREEN' | 'STEEL'; // 제품 카테고리 (스크린/철재) - partType?: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; // 부품 유형 (조립/절곡/구매) - partUsage?: 'GUIDE_RAIL' | 'BOTTOM_FINISH' | 'CASE' | 'DOOR' | 'BRACKET' | 'GENERAL'; // 부품 용도 - unit: string; - category1?: string; - category2?: string; - category3?: string; - specification?: string; - isVariableSize?: boolean; - isActive?: boolean; // 품목 활성/비활성 (제품/부품/원자재/부자재만 사용) - lotAbbreviation?: string; // 로트 약자 (제품만 사용) - purchasePrice?: number; - marginRate?: number; - processingCost?: number; - laborCost?: number; - installCost?: number; - salesPrice?: number; - safetyStock?: number; - leadTime?: number; - bom?: BOMLine[]; // 부품구성표(BOM) - 자재 명세서 - bomCategories?: string[]; // 견적산출용 샘플 제품의 BOM 카테고리 (예: ['motor', 'guide-rail']) - - // 인정 정보 - certificationNumber?: string; // 인정번호 - certificationStartDate?: string; // 인정 유효기간 시작일 - certificationEndDate?: string; // 인정 유효기간 종료일 - specificationFile?: string; // 시방서 파일 (Base64 또는 URL) - specificationFileName?: string; // 시방서 파일명 - certificationFile?: string; // 인정서 파일 (Base64 또는 URL) - certificationFileName?: string; // 인정서 파일명 - note?: string; // 비고 (제품만 사용) - - // 조립 부품 관련 필드 - installationType?: string; // 설치 유형 (wall: 벽면형, side: 측면형, steel: 스틸, iron: 철재) - assemblyType?: string; // 종류 (M, T, C, D, S, U 등) - sideSpecWidth?: string; // 측면 규격 가로 (mm) - sideSpecHeight?: string; // 측면 규격 세로 (mm) - assemblyLength?: string; // 길이 (2438, 3000, 3500, 4000, 4300 등) - - // 가이드레일 관련 필드 - guideRailModelType?: string; // 가이드레일 모델 유형 - guideRailModel?: string; // 가이드레일 모델 - - // 절곡품 관련 (부품 유형이 BENDING인 경우) - bendingDiagram?: string; // 전개도 이미지 URL - bendingDetails?: BendingDetail[]; // 전개도 상세 데이터 - material?: string; // 재질 (EGI 1.55T, SUS 1.2T 등) - length?: string; // 길이/목함 (mm) - - // 버전 관리 - currentRevision: number; // 현재 차수 (0 = 최초, 1 = 1차 수정...) - revisions?: ItemRevision[]; // 수정 이력 - isFinal: boolean; // 최종 확정 여부 - finalizedDate?: string; // 최종 확정일 - finalizedBy?: string; // 최종 확정자 - - createdAt: string; -} - -// 품목 기준정보 관리 (Master Data) -export interface ItemCategory { - id: string; - categoryType: 'PRODUCT' | 'PART' | 'MATERIAL' | 'SUB_MATERIAL'; // 품목 구분 - category1: string; // 대분류 - category2?: string; // 중분류 - category3?: string; // 소분류 - code?: string; // 코드 (자동생성 또는 수동입력) - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface ItemUnit { - id: string; - unitCode: string; // 단위 코드 (EA, SET, M, KG, L 등) - unitName: string; // 단위명 - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface ItemMaterial { - id: string; - materialCode: string; // 재질 코드 - materialName: string; // 재질명 (EGI 1.55T, SUS 1.2T 등) - materialType: 'STEEL' | 'ALUMINUM' | 'PLASTIC' | 'OTHER'; // 재질 유형 - thickness?: string; // 두께 (1.2T, 1.6T 등) - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface SurfaceTreatment { - id: string; - treatmentCode: string; // 처리 코드 - treatmentName: string; // 처리명 (무도장, 파우더도장, 아노다이징 등) - treatmentType: 'PAINTING' | 'COATING' | 'PLATING' | 'NONE'; // 처리 유형 - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface PartTypeOption { - id: string; - partType: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; // 부품 유형 - optionCode: string; // 옵션 코드 - optionName: string; // 옵션명 - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface PartUsageOption { - id: string; - usageCode: string; // 용도 코드 - usageName: string; // 용도명 (가이드레일, 하단마감재, 케이스 등) - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -export interface GuideRailOption { - id: string; - optionType: 'MODEL_TYPE' | 'MODEL' | 'CERTIFICATION' | 'SHAPE' | 'FINISH' | 'LENGTH'; // 옵션 유형 - optionCode: string; // 옵션 코드 - optionName: string; // 옵션명 - parentOption?: string; // 상위 옵션 (종속 관계) - description?: string; - isActive: boolean; - createdAt: string; - updatedAt?: string; -} - -// ===== 품목기준관리 계층구조 ===== - -// 항목 속성 -export interface ItemFieldProperty { - id?: string; // 속성 ID (properties 배열에서 사용) - key?: string; // 속성 키 (properties 배열에서 사용) - label?: string; // 속성 라벨 (properties 배열에서 사용) - type?: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'; // 속성 타입 (properties 배열에서 사용) - inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea' | 'section'; // 입력방식 - required: boolean; // 필수 여부 - row: number; // 행 위치 - col: number; // 열 위치 - options?: string[]; // 드롭다운 옵션 (입력방식이 dropdown일 경우) - defaultValue?: string; // 기본값 - placeholder?: string; // 플레이스홀더 - multiColumn?: boolean; // 다중 컬럼 사용 여부 - columnCount?: number; // 컬럼 개수 - columnNames?: string[]; // 각 컬럼의 이름 -} - -// 항목 마스터 (재사용 가능한 항목 템플릿) - MasterFieldResponse와 정확히 일치 -export interface ItemMasterField { - id: number; - tenant_id: number; - field_name: string; - field_key?: string | null; // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력}) - field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // API와 동일 - category: string | null; - description: string | null; - is_common: boolean; // 공통 필드 여부 - is_required?: boolean; // 필수 여부 (API에서 반환) - default_value: string | null; // 기본값 - options: Array<{ label: string; value: string }> | null; // dropdown 옵션 - validation_rules: Record | null; // 검증 규칙 - properties: Record | null; // 추가 속성 - created_by: number | null; - updated_by: number | null; - created_at: string; - updated_at: string; -} - -// 조건부 표시 설정 -export interface FieldDisplayCondition { - targetType: 'field' | 'section'; // 조건 대상 타입 - // 일반항목 조건 (여러 개 가능) - fieldConditions?: Array<{ - fieldKey: string; // 조건이 되는 필드의 키 - expectedValue: string; // 예상되는 값 - }>; - // 섹션 조건 (여러 개 가능) - sectionIds?: string[]; // 표시할 섹션 ID 배열 -} - -// 항목 (Field) - API 응답 구조에 맞춰 수정 -export interface ItemField { - id: number; // 서버 생성 ID (string → number) - tenant_id?: number; // 백엔드에서 자동 추가 - group_id?: number | null; // 그룹 ID (독립 필드용) - section_id: number | null; // 외래키 - 섹션 ID (독립 필드는 null) - master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우) - field_name: string; // 항목명 (name → field_name) - field_key?: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력}, 백엔드에서 생성) - field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입 - order_no: number; // 항목 순서 (order → order_no, required) - is_required: boolean; // 필수 여부 - placeholder?: string | null; // 플레이스홀더 - default_value?: string | null; // 기본값 - display_condition?: Record | null; // 조건부 표시 설정 (displayCondition → display_condition) - validation_rules?: Record | null; // 검증 규칙 - options?: Array<{ label: string; value: string }> | null; // dropdown 옵션 - properties?: Record | null; // 추가 속성 - // 2025-11-28 추가: 잠금 기능 - is_locked?: boolean; // 잠금 여부 - locked_by?: number | null; // 잠금 설정자 - locked_at?: string | null; // 잠금 시간 - created_by?: number | null; // 생성자 ID 추가 - updated_by?: number | null; // 수정자 ID 추가 - created_at: string; // 생성일 (camelCase → snake_case) - updated_at: string; // 수정일 추가 -} - -// BOM 아이템 타입 - API 응답 구조에 맞춰 수정 -export interface BOMItem { - id: number; // 서버 생성 ID (string → number) - tenant_id?: number; // 백엔드에서 자동 추가 - group_id?: number | null; // 그룹 ID (독립 BOM용) - section_id: number | null; // 외래키 - 섹션 ID (독립 BOM은 null) - item_code?: string | null; // 품목 코드 (itemCode → item_code, optional) - item_name: string; // 품목명 (itemName → item_name) - quantity: number; // 수량 - unit?: string | null; // 단위 (optional) - unit_price?: number | null; // 단가 추가 - total_price?: number | null; // 총액 추가 - spec?: string | null; // 규격/사양 추가 - note?: string | null; // 비고 (optional) - created_by?: number | null; // 생성자 ID 추가 - updated_by?: number | null; // 수정자 ID 추가 - created_at: string; // 생성일 (createdAt → created_at) - updated_at: string; // 수정일 추가 -} - -// 섹션 (Section) - API 응답 구조에 맞춰 수정 -export interface ItemSection { - id: number; // 서버 생성 ID (string → number) - tenant_id?: number; // 백엔드에서 자동 추가 - group_id?: number | null; // 그룹 ID (독립 섹션 그룹화용) - 2025-11-26 추가 - page_id: number | null; // 외래키 - 페이지 ID (null이면 독립 섹션) - 2025-11-26 수정 - title: string; // 섹션 제목 (API 필드명과 일치하도록 section_name → title) - section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // 섹션 타입 (type → section_type, 값 변경) - description?: string | null; // 설명 - order_no: number; // 섹션 순서 (order → order_no) - is_template: boolean; // 템플릿 여부 (section_templates 통합) - 2025-11-26 추가 - is_default: boolean; // 기본 템플릿 여부 - 2025-11-26 추가 - is_collapsible?: boolean; // 접기/펼치기 가능 여부 (프론트엔드 전용, optional) - is_default_open?: boolean; // 기본 열림 상태 (프론트엔드 전용, optional) - created_by?: number | null; // 생성자 ID 추가 - updated_by?: number | null; // 수정자 ID 추가 - created_at: string; // 생성일 (camelCase → snake_case) - updated_at: string; // 수정일 추가 - fields?: ItemField[]; // 섹션에 포함된 항목들 (optional로 변경) - bom_items?: BOMItem[]; // BOM 타입일 경우 BOM 품목 목록 (bomItems → bom_items) -} - -// 페이지 (Page) - API 응답 구조에 맞춰 수정 -export interface ItemPage { - id: number; // 서버 생성 ID (string → number) - tenant_id?: number; // 백엔드에서 자동 추가 - page_name: string; // 페이지명 (camelCase → snake_case) - item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 품목유형 - description?: string | null; // 설명 추가 - absolute_path: string; // 절대경로 (camelCase → snake_case) - is_active: boolean; // 사용 여부 (camelCase → snake_case) - order_no: number; // 순서 번호 추가 - created_by?: number | null; // 생성자 ID 추가 - updated_by?: number | null; // 수정자 ID 추가 - created_at: string; // 생성일 (camelCase → snake_case) - updated_at: string; // 수정일 (camelCase → snake_case) - sections: ItemSection[]; // 페이지에 포함된 섹션들 (Nested) -} - -// 템플릿 필드 (로컬 관리용 - API에서 제공하지 않음) -export interface TemplateField { - id: string; - name: string; - fieldKey: string; - property: { - inputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; - required: boolean; - options?: string[]; - multiColumn?: boolean; - columnCount?: number; - columnNames?: string[]; - }; - description?: string; -} - -// 섹션 템플릿 (재사용 가능한 섹션) - Transformer 출력과 UI 요구사항에 맞춤 -export interface SectionTemplate { - id: number; - tenant_id: number; - template_name: string; // transformer가 title → template_name으로 변환 - section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // transformer가 type → section_type으로 변환 - description: string | null; - default_fields: TemplateField[] | null; // 기본 필드 (로컬 관리) - category?: string[]; // 적용 카테고리 (로컬 관리) - fields?: TemplateField[]; // 템플릿에 포함된 필드 (로컬 관리) - bomItems?: BOMItem[]; // BOM 타입일 경우 BOM 품목 (로컬 관리) - created_by: number | null; - updated_by: number | null; - created_at: string; - updated_at: string; -} +import type { + BendingDetail, + BOMLine, + SpecificationMaster, + MaterialItemName, + ItemRevision, + ItemMaster, + ItemCategory, + ItemUnit, + ItemMaterial, + SurfaceTreatment, + PartTypeOption, + PartUsageOption, + GuideRailOption, + ItemFieldProperty, + ItemMasterField, + FieldDisplayCondition, + ItemField, + BOMItem, + ItemSection, + ItemPage, + TemplateField, + SectionTemplate, +} from '@/types/item-master.types'; // ===== Context Type ===== interface ItemMasterContextType { @@ -1295,11 +959,22 @@ export function ItemMasterProvider({ children }: { children: ReactNode }) { throw new Error(response.message || '페이지 수정 실패'); } - // 응답 데이터 변환 및 state 업데이트 - const updatedPage = transformPageResponse(response.data); - setItemPages(prev => prev.map(page => page.id === id ? updatedPage : page)); + // ⚠️ 2026-01-06: 변경 요청한 필드만 업데이트 + // API 응답(response.data)에 sections가 빈 배열로 오기 때문에 + // 응답 전체를 덮어쓰면 기존 섹션이 사라지는 버그 발생 + // → 변경한 필드(page_name, absolute_path)만 업데이트하고 나머지는 기존 값 유지 + setItemPages(prev => prev.map(page => { + if (page.id === id) { + return { + ...page, + page_name: updates.page_name ?? page.page_name, + absolute_path: updates.absolute_path ?? page.absolute_path, + }; + } + return page; + })); - console.log('[ItemMasterContext] 페이지 수정 성공:', updatedPage); + console.log('[ItemMasterContext] 페이지 수정 성공:', { id, updates }); } catch (error) { const errorMessage = getErrorMessage(error); console.error('[ItemMasterContext] 페이지 수정 실패:', errorMessage); diff --git a/src/stores/item-master/useItemMasterStore.ts b/src/stores/item-master/useItemMasterStore.ts index 15f038a0..2f8ce512 100644 --- a/src/stores/item-master/useItemMasterStore.ts +++ b/src/stores/item-master/useItemMasterStore.ts @@ -124,16 +124,26 @@ export const useItemMasterStore = create()( updatePage: async (id, updates) => { try { + console.log('[ItemMasterStore] updatePage 시작:', { id, updates }); + // ✅ Phase 3: API 연동 const apiData = denormalizePageForRequest(updates); - const response = await itemMasterApi.pages.update(id, apiData); + await itemMasterApi.pages.update(id, apiData); - // API 응답으로 로컬 상태 업데이트 + // ✅ 변경된 필드만 로컬 상태 업데이트 (sectionIds는 건드리지 않음!) + // API 응답에 sections가 빈 배열로 오기 때문에 initFromApi() 사용 안 함 set((state) => { - if (state.entities.pages[id]) { - Object.assign(state.entities.pages[id], updates, { - updated_at: response.data?.updated_at || new Date().toISOString(), - }); + const page = state.entities.pages[id]; + if (page) { + // 변경 요청된 필드들만 업데이트 + if (updates.page_name !== undefined) page.page_name = updates.page_name; + if (updates.description !== undefined) page.description = updates.description; + if (updates.item_type !== undefined) page.item_type = updates.item_type; + if (updates.absolute_path !== undefined) page.absolute_path = updates.absolute_path; + if (updates.is_active !== undefined) page.is_active = updates.is_active; + if (updates.order_no !== undefined) page.order_no = updates.order_no; + // sectionIds는 건드리지 않음 - 페이지 정보만 수정한 거니까! + page.updated_at = new Date().toISOString(); } }); @@ -243,16 +253,28 @@ export const useItemMasterStore = create()( updateSection: async (id, updates) => { try { + console.log('[ItemMasterStore] updateSection 시작:', { id, updates }); + // ✅ Phase 3: API 연동 const apiData = denormalizeSectionForRequest(updates); - const response = await itemMasterApi.sections.update(id, apiData); + await itemMasterApi.sections.update(id, apiData); - // ⭐ 핵심: 1곳만 수정하면 끝! + // ✅ 변경된 필드만 로컬 상태 업데이트 (fieldIds, bomItemIds는 건드리지 않음!) + // API 응답에 fields가 빈 배열로 오기 때문에 initFromApi() 사용 안 함 set((state) => { - if (state.entities.sections[id]) { - Object.assign(state.entities.sections[id], updates, { - updated_at: response.data?.updated_at || new Date().toISOString(), - }); + const section = state.entities.sections[id]; + if (section) { + // 변경 요청된 필드들만 업데이트 + if (updates.title !== undefined) section.title = updates.title; + if (updates.description !== undefined) section.description = updates.description; + if (updates.section_type !== undefined) section.section_type = updates.section_type; + if (updates.order_no !== undefined) section.order_no = updates.order_no; + if (updates.is_template !== undefined) section.is_template = updates.is_template; + if (updates.is_default !== undefined) section.is_default = updates.is_default; + if (updates.is_collapsible !== undefined) section.is_collapsible = updates.is_collapsible; + if (updates.is_default_open !== undefined) section.is_default_open = updates.is_default_open; + // fieldIds, bomItemIds는 건드리지 않음 - 섹션 정보만 수정한 거니까! + section.updated_at = new Date().toISOString(); } }); diff --git a/src/types/item-master.types.ts b/src/types/item-master.types.ts new file mode 100644 index 00000000..dbb3ac39 --- /dev/null +++ b/src/types/item-master.types.ts @@ -0,0 +1,392 @@ +/** + * 품목기준관리 타입 정의 + * ItemMasterContext에서 분리됨 (2026-01-06) + */ + +// ===== 기본 타입 ===== + +// 전개도 상세 정보 +export interface BendingDetail { + id: string; + no: number; // 번호 + input: number; // 입력 + elongation: number; // 연신율 (기본값 -1) + calculated: number; // 연신율 계산 후 + sum: number; // 합계 + shaded: boolean; // 음영 여부 + aAngle?: number; // A각 +} + +// 부품구성표(BOM, Bill of Materials) - 자재 명세서 +export interface BOMLine { + id: string; + childItemCode: string; // 구성 품목 코드 + childItemName: string; // 구성 품목명 + quantity: number; // 기준 수량 + unit: string; // 단위 + unitPrice?: number; // 단가 + quantityFormula?: string; // 수량 계산식 (예: "W * 2", "H + 100") + note?: string; // 비고 + // 절곡품 관련 (하위 절곡 부품용) + isBending?: boolean; + bendingDiagram?: string; // 전개도 이미지 URL + bendingDetails?: BendingDetail[]; // 전개도 상세 데이터 +} + +// 규격 마스터 (원자재/부자재용) +export interface SpecificationMaster { + id: string; + specificationCode: string; // 규격 코드 (예: 1.6T x 1219 x 2438) + itemType: 'RM' | 'SM'; // 원자재 | 부자재 + itemName?: string; // 품목명 (예: SPHC-SD, SPCC-SD) - 품목명별 규격 필터링용 + fieldCount: '1' | '2' | '3'; // 너비 입력 개수 + thickness: string; // 두께 + widthA: string; // 너비A + widthB?: string; // 너비B + widthC?: string; // 너비C + length: string; // 길이 + description?: string; // 설명 + isActive: boolean; // 활성 여부 + createdAt?: string; + updatedAt?: string; +} + +// 원자재/부자재 품목명 마스터 +export interface MaterialItemName { + id: string; + itemType: 'RM' | 'SM'; // 원자재 | 부자재 + itemName: string; // 품목명 (예: "SPHC-SD", "STS430") + category?: string; // 분류 (예: "냉연", "열연", "스테인리스") + description?: string; // 설명 + isActive: boolean; // 활성 여부 + createdAt: string; + updatedAt?: string; +} + +// 품목 수정 이력 +export interface ItemRevision { + revisionNumber: number; // 수정 차수 (1차, 2차, 3차...) + revisionDate: string; // 수정일 + revisionBy: string; // 수정자 + revisionReason?: string; // 수정 사유 + previousData: any; // 이전 버전의 전체 데이터 +} + +// 품목 마스터 +export interface ItemMaster { + id: string; + itemCode: string; + itemName: string; + itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 제품, 부품, 부자재, 원자재, 소모품 + productCategory?: 'SCREEN' | 'STEEL'; // 제품 카테고리 (스크린/철재) + partType?: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; // 부품 유형 (조립/절곡/구매) + partUsage?: 'GUIDE_RAIL' | 'BOTTOM_FINISH' | 'CASE' | 'DOOR' | 'BRACKET' | 'GENERAL'; // 부품 용도 + unit: string; + category1?: string; + category2?: string; + category3?: string; + specification?: string; + isVariableSize?: boolean; + isActive?: boolean; // 품목 활성/비활성 (제품/부품/원자재/부자재만 사용) + lotAbbreviation?: string; // 로트 약자 (제품만 사용) + purchasePrice?: number; + marginRate?: number; + processingCost?: number; + laborCost?: number; + installCost?: number; + salesPrice?: number; + safetyStock?: number; + leadTime?: number; + bom?: BOMLine[]; // 부품구성표(BOM) - 자재 명세서 + bomCategories?: string[]; // 견적산출용 샘플 제품의 BOM 카테고리 (예: ['motor', 'guide-rail']) + + // 인정 정보 + certificationNumber?: string; // 인정번호 + certificationStartDate?: string; // 인정 유효기간 시작일 + certificationEndDate?: string; // 인정 유효기간 종료일 + specificationFile?: string; // 시방서 파일 (Base64 또는 URL) + specificationFileName?: string; // 시방서 파일명 + certificationFile?: string; // 인정서 파일 (Base64 또는 URL) + certificationFileName?: string; // 인정서 파일명 + note?: string; // 비고 (제품만 사용) + + // 조립 부품 관련 필드 + installationType?: string; // 설치 유형 (wall: 벽면형, side: 측면형, steel: 스틸, iron: 철재) + assemblyType?: string; // 종류 (M, T, C, D, S, U 등) + sideSpecWidth?: string; // 측면 규격 가로 (mm) + sideSpecHeight?: string; // 측면 규격 세로 (mm) + assemblyLength?: string; // 길이 (2438, 3000, 3500, 4000, 4300 등) + + // 가이드레일 관련 필드 + guideRailModelType?: string; // 가이드레일 모델 유형 + guideRailModel?: string; // 가이드레일 모델 + + // 절곡품 관련 (부품 유형이 BENDING인 경우) + bendingDiagram?: string; // 전개도 이미지 URL + bendingDetails?: BendingDetail[]; // 전개도 상세 데이터 + material?: string; // 재질 (EGI 1.55T, SUS 1.2T 등) + length?: string; // 길이/목함 (mm) + + // 버전 관리 + currentRevision: number; // 현재 차수 (0 = 최초, 1 = 1차 수정...) + revisions?: ItemRevision[]; // 수정 이력 + isFinal: boolean; // 최종 확정 여부 + finalizedDate?: string; // 최종 확정일 + finalizedBy?: string; // 최종 확정자 + + createdAt: string; +} + +// ===== 품목 기준정보 관리 (Master Data) ===== + +export interface ItemCategory { + id: string; + categoryType: 'PRODUCT' | 'PART' | 'MATERIAL' | 'SUB_MATERIAL'; // 품목 구분 + category1: string; // 대분류 + category2?: string; // 중분류 + category3?: string; // 소분류 + code?: string; // 코드 (자동생성 또는 수동입력) + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface ItemUnit { + id: string; + unitCode: string; // 단위 코드 (EA, SET, M, KG, L 등) + unitName: string; // 단위명 + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface ItemMaterial { + id: string; + materialCode: string; // 재질 코드 + materialName: string; // 재질명 (EGI 1.55T, SUS 1.2T 등) + materialType: 'STEEL' | 'ALUMINUM' | 'PLASTIC' | 'OTHER'; // 재질 유형 + thickness?: string; // 두께 (1.2T, 1.6T 등) + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface SurfaceTreatment { + id: string; + treatmentCode: string; // 처리 코드 + treatmentName: string; // 처리명 (무도장, 파우더도장, 아노다이징 등) + treatmentType: 'PAINTING' | 'COATING' | 'PLATING' | 'NONE'; // 처리 유형 + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface PartTypeOption { + id: string; + partType: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; // 부품 유형 + optionCode: string; // 옵션 코드 + optionName: string; // 옵션명 + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface PartUsageOption { + id: string; + usageCode: string; // 용도 코드 + usageName: string; // 용도명 (가이드레일, 하단마감재, 케이스 등) + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface GuideRailOption { + id: string; + optionType: 'MODEL_TYPE' | 'MODEL' | 'CERTIFICATION' | 'SHAPE' | 'FINISH' | 'LENGTH'; // 옵션 유형 + optionCode: string; // 옵션 코드 + optionName: string; // 옵션명 + parentOption?: string; // 상위 옵션 (종속 관계) + description?: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +// ===== 품목기준관리 계층구조 ===== + +// 항목 속성 +export interface ItemFieldProperty { + id?: string; // 속성 ID (properties 배열에서 사용) + key?: string; // 속성 키 (properties 배열에서 사용) + label?: string; // 속성 라벨 (properties 배열에서 사용) + type?: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea'; // 속성 타입 (properties 배열에서 사용) + inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea' | 'section'; // 입력방식 + required: boolean; // 필수 여부 + row: number; // 행 위치 + col: number; // 열 위치 + options?: string[]; // 드롭다운 옵션 (입력방식이 dropdown일 경우) + defaultValue?: string; // 기본값 + placeholder?: string; // 플레이스홀더 + multiColumn?: boolean; // 다중 컬럼 사용 여부 + columnCount?: number; // 컬럼 개수 + columnNames?: string[]; // 각 컬럼의 이름 +} + +// 항목 마스터 (재사용 가능한 항목 템플릿) - MasterFieldResponse와 정확히 일치 +export interface ItemMasterField { + id: number; + tenant_id: number; + field_name: string; + field_key?: string | null; // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력}) + field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // API와 동일 + category: string | null; + description: string | null; + is_common: boolean; // 공통 필드 여부 + is_required?: boolean; // 필수 여부 (API에서 반환) + default_value: string | null; // 기본값 + options: Array<{ label: string; value: string }> | null; // dropdown 옵션 + validation_rules: Record | null; // 검증 규칙 + properties: Record | null; // 추가 속성 + created_by: number | null; + updated_by: number | null; + created_at: string; + updated_at: string; +} + +// 조건부 표시 설정 +export interface FieldDisplayCondition { + targetType: 'field' | 'section'; // 조건 대상 타입 + // 일반항목 조건 (여러 개 가능) + fieldConditions?: Array<{ + fieldKey: string; // 조건이 되는 필드의 키 + expectedValue: string; // 예상되는 값 + }>; + // 섹션 조건 (여러 개 가능) + sectionIds?: string[]; // 표시할 섹션 ID 배열 +} + +// 항목 (Field) - API 응답 구조에 맞춰 수정 +export interface ItemField { + id: number; // 서버 생성 ID (string → number) + tenant_id?: number; // 백엔드에서 자동 추가 + group_id?: number | null; // 그룹 ID (독립 필드용) + section_id: number | null; // 외래키 - 섹션 ID (독립 필드는 null) + master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우) + field_name: string; // 항목명 (name → field_name) + field_key?: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력}, 백엔드에서 생성) + field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; // 필드 타입 + order_no: number; // 항목 순서 (order → order_no, required) + is_required: boolean; // 필수 여부 + placeholder?: string | null; // 플레이스홀더 + default_value?: string | null; // 기본값 + display_condition?: Record | null; // 조건부 표시 설정 (displayCondition → display_condition) + validation_rules?: Record | null; // 검증 규칙 + options?: Array<{ label: string; value: string }> | null; // dropdown 옵션 + properties?: Record | null; // 추가 속성 + // 2025-11-28 추가: 잠금 기능 + is_locked?: boolean; // 잠금 여부 + locked_by?: number | null; // 잠금 설정자 + locked_at?: string | null; // 잠금 시간 + created_by?: number | null; // 생성자 ID 추가 + updated_by?: number | null; // 수정자 ID 추가 + created_at: string; // 생성일 (camelCase → snake_case) + updated_at: string; // 수정일 추가 +} + +// BOM 아이템 타입 - API 응답 구조에 맞춰 수정 +export interface BOMItem { + id: number; // 서버 생성 ID (string → number) + tenant_id?: number; // 백엔드에서 자동 추가 + group_id?: number | null; // 그룹 ID (독립 BOM용) + section_id: number | null; // 외래키 - 섹션 ID (독립 BOM은 null) + item_code?: string | null; // 품목 코드 (itemCode → item_code, optional) + item_name: string; // 품목명 (itemName → item_name) + quantity: number; // 수량 + unit?: string | null; // 단위 (optional) + unit_price?: number | null; // 단가 추가 + total_price?: number | null; // 총액 추가 + spec?: string | null; // 규격/사양 추가 + note?: string | null; // 비고 (optional) + created_by?: number | null; // 생성자 ID 추가 + updated_by?: number | null; // 수정자 ID 추가 + created_at: string; // 생성일 (createdAt → created_at) + updated_at: string; // 수정일 추가 +} + +// 섹션 (Section) - API 응답 구조에 맞춰 수정 +export interface ItemSection { + id: number; // 서버 생성 ID (string → number) + tenant_id?: number; // 백엔드에서 자동 추가 + group_id?: number | null; // 그룹 ID (독립 섹션 그룹화용) - 2025-11-26 추가 + page_id: number | null; // 외래키 - 페이지 ID (null이면 독립 섹션) - 2025-11-26 수정 + title: string; // 섹션 제목 (API 필드명과 일치하도록 section_name → title) + section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // 섹션 타입 (type → section_type, 값 변경) + description?: string | null; // 설명 + order_no: number; // 섹션 순서 (order → order_no) + is_template: boolean; // 템플릿 여부 (section_templates 통합) - 2025-11-26 추가 + is_default: boolean; // 기본 템플릿 여부 - 2025-11-26 추가 + is_collapsible?: boolean; // 접기/펼치기 가능 여부 (프론트엔드 전용, optional) + is_default_open?: boolean; // 기본 열림 상태 (프론트엔드 전용, optional) + created_by?: number | null; // 생성자 ID 추가 + updated_by?: number | null; // 수정자 ID 추가 + created_at: string; // 생성일 (camelCase → snake_case) + updated_at: string; // 수정일 추가 + fields?: ItemField[]; // 섹션에 포함된 항목들 (optional로 변경) + bom_items?: BOMItem[]; // BOM 타입일 경우 BOM 품목 목록 (bomItems → bom_items) +} + +// 페이지 (Page) - API 응답 구조에 맞춰 수정 +export interface ItemPage { + id: number; // 서버 생성 ID (string → number) + tenant_id?: number; // 백엔드에서 자동 추가 + page_name: string; // 페이지명 (camelCase → snake_case) + item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 품목유형 + description?: string | null; // 설명 추가 + absolute_path: string; // 절대경로 (camelCase → snake_case) + is_active: boolean; // 사용 여부 (camelCase → snake_case) + order_no: number; // 순서 번호 추가 + created_by?: number | null; // 생성자 ID 추가 + updated_by?: number | null; // 수정자 ID 추가 + created_at: string; // 생성일 (camelCase → snake_case) + updated_at: string; // 수정일 (camelCase → snake_case) + sections: ItemSection[]; // 페이지에 포함된 섹션들 (Nested) +} + +// 템플릿 필드 (로컬 관리용 - API에서 제공하지 않음) +export interface TemplateField { + id: string; + name: string; + fieldKey: string; + property: { + inputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'; + required: boolean; + options?: string[]; + multiColumn?: boolean; + columnCount?: number; + columnNames?: string[]; + }; + description?: string; +} + +// 섹션 템플릿 (재사용 가능한 섹션) - Transformer 출력과 UI 요구사항에 맞춤 +export interface SectionTemplate { + id: number; + tenant_id: number; + template_name: string; // transformer가 title → template_name으로 변환 + section_type: 'BASIC' | 'BOM' | 'CUSTOM'; // transformer가 type → section_type으로 변환 + description: string | null; + default_fields: TemplateField[] | null; // 기본 필드 (로컬 관리) + category?: string[]; // 적용 카테고리 (로컬 관리) + fields?: TemplateField[]; // 템플릿에 포함된 필드 (로컬 관리) + bomItems?: BOMItem[]; // BOM 타입일 경우 BOM 품목 (로컬 관리) + created_by: number | null; + updated_by: number | null; + created_at: string; + updated_at: string; +} \ No newline at end of file