feat: 422 ValidationException 에러 AlertDialog 팝업 추가

- ErrorAlertContext 생성 (전역 에러 알림 상태 관리)
- useFieldManagement, useMasterFieldManagement, useTemplateManagement에 적용
- 중복 이름, 예약어 사용 시 디자인된 AlertDialog 팝업 표시
- toast 대신 모달 위에 표시되는 팝업으로 변경

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-06 20:13:06 +09:00
parent e5098b0880
commit 3b52847d89
12 changed files with 821 additions and 417 deletions

View File

@@ -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) => ({

View File

@@ -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 (
<ErrorAlertProvider>
<ItemMasterDataManagementContent />
</ErrorAlertProvider>
);
}
// 실제 로직을 담는 내부 컴포넌트
function ItemMasterDataManagementContent() {
const {
itemPages,
loadItemPages: _loadItemPages,

View File

@@ -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 (
<AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
{title}
</AlertDialogTitle>
<AlertDialogDescription className="text-base">
{message}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={onClose}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -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<ErrorAlertContextType | null>(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<ErrorAlertState>({
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 (
<ErrorAlertContext.Provider value={{ showErrorAlert }}>
{children}
{/* 에러 알림 다이얼로그 */}
<AlertDialog open={errorAlert.open} onOpenChange={(isOpen) => !isOpen && closeErrorAlert()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
{errorAlert.title}
</AlertDialogTitle>
<AlertDialogDescription className="text-base text-foreground">
{errorAlert.message}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={closeErrorAlert}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ErrorAlertContext.Provider>
);
}

View File

@@ -0,0 +1 @@
export { ErrorAlertProvider, useErrorAlert } from './ErrorAlertContext';

View File

@@ -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<ErrorAlertState>({
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,
};
}

View File

@@ -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<number | null>(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('항목 처리에 실패했습니다', '오류');
}
}
};

View File

@@ -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<React.SetStateAction<string[]>>;
// 핸들러
handleAddMasterField: () => void;
handleAddMasterField: () => Promise<void>;
handleEditMasterField: (field: ItemMasterField) => void;
handleUpdateMasterField: () => void;
handleDeleteMasterField: (id: number) => void;
handleUpdateMasterField: () => Promise<void>;
handleDeleteMasterField: (id: number) => Promise<void>;
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<number | null>(null);
@@ -77,7 +82,7 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
const [newMasterFieldColumnNames, setNewMasterFieldColumnNames] = useState<string[]>(['컬럼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('항목 삭제에 실패했습니다');
}
}
}
};

View File

@@ -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<number | null>(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('항목 처리에 실패했습니다', '오류');
}
}
};

View File

@@ -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<string, any> | null; // 검증 규칙
properties: Record<string, any> | 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<string, any> | null; // 조건부 표시 설정 (displayCondition → display_condition)
validation_rules?: Record<string, any> | null; // 검증 규칙
options?: Array<{ label: string; value: string }> | null; // dropdown 옵션
properties?: Record<string, any> | 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);

View File

@@ -124,16 +124,26 @@ export const useItemMasterStore = create<ItemMasterStore>()(
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<ItemMasterStore>()(
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();
}
});

View File

@@ -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<string, any> | null; // 검증 규칙
properties: Record<string, any> | 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<string, any> | null; // 조건부 표시 설정 (displayCondition → display_condition)
validation_rules?: Record<string, any> | null; // 검증 규칙
options?: Array<{ label: string; value: string }> | null; // dropdown 옵션
properties?: Record<string, any> | 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;
}