- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext) - HeadersInit → Record<string, string> 타입 변경 - Zustand useShallow 마이그레이션 (zustand/react/shallow) - DataTable, ListPageTemplate 제네릭 타입 제약 추가 - 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한) - HR 관리 페이지 추가 (급여, 휴가) - 단가관리 페이지 리팩토링 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2732 lines
105 KiB
TypeScript
2732 lines
105 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useState, useEffect, useMemo, ReactNode } from 'react';
|
|
import { useAuth } from './AuthContext';
|
|
import { TenantAwareCache } from '@/lib/cache';
|
|
import { itemMasterApi } from '@/lib/api/item-master';
|
|
import { getErrorMessage, ApiError } from '@/lib/api/error-handler';
|
|
import {
|
|
transformPageResponse,
|
|
transformSectionResponse,
|
|
transformFieldResponse,
|
|
transformBomItemResponse,
|
|
} from '@/lib/api/transformers';
|
|
import type {
|
|
ItemPageRequest,
|
|
IndependentSectionRequest,
|
|
IndependentFieldRequest,
|
|
IndependentBomItemRequest,
|
|
LinkSectionRequest,
|
|
LinkFieldRequest,
|
|
SectionUsageResponse,
|
|
FieldUsageResponse,
|
|
} from '@/types/item-master-api';
|
|
|
|
// ===== Type Definitions =====
|
|
|
|
// 전개도 상세 정보
|
|
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;
|
|
}
|
|
|
|
// ===== Context Type =====
|
|
interface ItemMasterContextType {
|
|
// 품목 마스터 데이터
|
|
itemMasters: ItemMaster[];
|
|
addItemMaster: (item: ItemMaster) => void;
|
|
updateItemMaster: (id: string, updates: Partial<ItemMaster>) => void;
|
|
deleteItemMaster: (id: string) => void;
|
|
|
|
// 규격 마스터 데이터 (원자재/부자재)
|
|
specificationMasters: SpecificationMaster[];
|
|
addSpecificationMaster: (spec: SpecificationMaster) => void;
|
|
updateSpecificationMaster: (id: string, updates: Partial<SpecificationMaster>) => void;
|
|
deleteSpecificationMaster: (id: string) => void;
|
|
|
|
// 원자재/부자재 품목명 마스터 데이터
|
|
materialItemNames: MaterialItemName[];
|
|
addMaterialItemName: (item: MaterialItemName) => void;
|
|
updateMaterialItemName: (id: string, updates: Partial<MaterialItemName>) => void;
|
|
deleteMaterialItemName: (id: string) => void;
|
|
|
|
// 품목기준관리 - 마스터 항목
|
|
itemMasterFields: ItemMasterField[];
|
|
loadItemMasterFields: (fields: ItemMasterField[]) => void; // 초기 데이터 로딩용 (API 호출 없음)
|
|
addItemMasterField: (field: Omit<ItemMasterField, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
|
updateItemMasterField: (id: number, updates: Partial<ItemMasterField>) => Promise<void>;
|
|
deleteItemMasterField: (id: number) => Promise<void>;
|
|
|
|
// 품목기준관리 - 섹션 템플릿
|
|
sectionTemplates: SectionTemplate[];
|
|
loadSectionTemplates: (templates: SectionTemplate[]) => void; // 초기 데이터 로딩용 (API 호출 없음)
|
|
addSectionTemplate: (template: Omit<SectionTemplate, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
|
updateSectionTemplate: (id: number, updates: Partial<SectionTemplate>) => Promise<void>;
|
|
deleteSectionTemplate: (id: number) => Promise<void>;
|
|
|
|
// 품목기준관리 계층구조
|
|
itemPages: ItemPage[];
|
|
loadItemPages: (pages: ItemPage[]) => void; // 초기 데이터 로딩용 (API 호출 없음)
|
|
addItemPage: (page: Omit<ItemPage, 'id' | 'created_at' | 'updated_at'>) => Promise<ItemPage>;
|
|
updateItemPage: (id: number, updates: Partial<ItemPage>) => Promise<void>;
|
|
deleteItemPage: (id: number) => Promise<void>;
|
|
reorderPages: (newOrder: Array<{ id: number; order_no: number }>) => Promise<void>;
|
|
addSectionToPage: (pageId: number, sectionData: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
|
updateSection: (sectionId: number, updates: Partial<ItemSection>) => Promise<void>;
|
|
deleteSection: (sectionId: number) => Promise<void>;
|
|
reorderSections: (pageId: number, sectionIds: number[]) => Promise<void>;
|
|
addFieldToSection: (sectionId: number, fieldData: Omit<ItemField, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
|
updateField: (fieldId: number, updates: Partial<ItemField>) => Promise<void>;
|
|
deleteField: (fieldId: number) => Promise<void>;
|
|
reorderFields: (sectionId: number, fieldIds: number[]) => Promise<void>;
|
|
|
|
// BOM 관리
|
|
addBOMItem: (sectionId: number, bomData: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => Promise<void>;
|
|
updateBOMItem: (bomId: number, updates: Partial<BOMItem>) => Promise<void>;
|
|
deleteBOMItem: (bomId: number) => Promise<void>;
|
|
|
|
// 독립 엔티티 관리 (2025-11-26 추가)
|
|
independentSections: ItemSection[]; // 독립 섹션 목록 (page_id=null)
|
|
independentFields: ItemField[]; // 독립 필드 목록 (section_id=null)
|
|
independentBomItems: BOMItem[]; // 독립 BOM 목록 (section_id=null)
|
|
loadIndependentSections: (sections: ItemSection[]) => void;
|
|
loadIndependentFields: (fields: ItemField[]) => void;
|
|
loadIndependentBomItems: (bomItems: BOMItem[]) => void;
|
|
refreshIndependentSections: (isTemplate?: boolean) => Promise<void>;
|
|
refreshIndependentFields: () => Promise<void>;
|
|
refreshIndependentBomItems: () => Promise<void>;
|
|
createIndependentSection: (data: Omit<IndependentSectionRequest, 'group_id'>) => Promise<ItemSection>;
|
|
createIndependentField: (data: Omit<IndependentFieldRequest, 'group_id'>) => Promise<ItemField>;
|
|
createIndependentBomItem: (data: Omit<IndependentBomItemRequest, 'group_id'>) => Promise<BOMItem>;
|
|
|
|
// 링크/언링크 관리 (2025-11-26 추가)
|
|
linkSectionToPage: (pageId: number, sectionId: number, orderNo?: number) => Promise<void>;
|
|
unlinkSectionFromPage: (pageId: number, sectionId: number) => Promise<void>;
|
|
linkFieldToSection: (sectionId: number, fieldId: number, orderNo?: number, fieldData?: ItemField) => Promise<void>;
|
|
unlinkFieldFromSection: (sectionId: number, fieldId: number) => Promise<void>;
|
|
|
|
// 사용처 조회 (2025-11-26 추가)
|
|
getSectionUsage: (sectionId: number) => Promise<SectionUsageResponse>;
|
|
getFieldUsage: (fieldId: number) => Promise<FieldUsageResponse>;
|
|
|
|
// 복제 (2025-11-26 추가)
|
|
cloneSection: (sectionId: number) => Promise<ItemSection>;
|
|
cloneField: (fieldId: number) => Promise<ItemField>;
|
|
|
|
// 품목 기준정보 관리
|
|
itemCategories: ItemCategory[];
|
|
itemUnits: ItemUnit[];
|
|
itemMaterials: ItemMaterial[];
|
|
surfaceTreatments: SurfaceTreatment[];
|
|
partTypeOptions: PartTypeOption[];
|
|
partUsageOptions: PartUsageOption[];
|
|
guideRailOptions: GuideRailOption[];
|
|
|
|
addItemCategory: (category: ItemCategory) => void;
|
|
updateItemCategory: (id: string, updates: Partial<ItemCategory>) => void;
|
|
deleteItemCategory: (id: string) => void;
|
|
|
|
addItemUnit: (unit: ItemUnit) => void;
|
|
updateItemUnit: (id: string, updates: Partial<ItemUnit>) => void;
|
|
deleteItemUnit: (id: string) => void;
|
|
|
|
addItemMaterial: (material: ItemMaterial) => void;
|
|
updateItemMaterial: (id: string, updates: Partial<ItemMaterial>) => void;
|
|
deleteItemMaterial: (id: string) => void;
|
|
|
|
addSurfaceTreatment: (treatment: SurfaceTreatment) => void;
|
|
updateSurfaceTreatment: (id: string, updates: Partial<SurfaceTreatment>) => void;
|
|
deleteSurfaceTreatment: (id: string) => void;
|
|
|
|
addPartTypeOption: (option: PartTypeOption) => void;
|
|
updatePartTypeOption: (id: string, updates: Partial<PartTypeOption>) => void;
|
|
deletePartTypeOption: (id: string) => void;
|
|
|
|
addPartUsageOption: (option: PartUsageOption) => void;
|
|
updatePartUsageOption: (id: string, updates: Partial<PartUsageOption>) => void;
|
|
deletePartUsageOption: (id: string) => void;
|
|
|
|
addGuideRailOption: (option: GuideRailOption) => void;
|
|
updateGuideRailOption: (id: string, updates: Partial<GuideRailOption>) => void;
|
|
deleteGuideRailOption: (id: string) => void;
|
|
|
|
// 캐시 및 데이터 초기화
|
|
clearCache: () => void; // TenantAwareCache 정리
|
|
resetAllData: () => void; // 모든 state 초기화
|
|
|
|
// 테넌트 정보
|
|
tenantId: number | undefined; // 현재 로그인한 사용자의 테넌트 ID
|
|
}
|
|
|
|
// Create context
|
|
const ItemMasterContext = createContext<ItemMasterContextType | undefined>(undefined);
|
|
|
|
// Provider component
|
|
export function ItemMasterProvider({ children }: { children: ReactNode }) {
|
|
// ===== Initial Data =====
|
|
// ✅ Mock 데이터 주석 처리 - API에서 가져올 예정
|
|
const initialItemMasters: ItemMaster[] = [];
|
|
|
|
// ✅ 빈 배열로 초기화 - API에서 데이터 로드 예정
|
|
const initialSpecificationMasters: SpecificationMaster[] = [];
|
|
const initialMaterialItemNames: MaterialItemName[] = [];
|
|
const initialItemCategories: ItemCategory[] = [];
|
|
const initialItemUnits: ItemUnit[] = [];
|
|
const initialItemMaterials: ItemMaterial[] = [];
|
|
const initialSurfaceTreatments: SurfaceTreatment[] = [];
|
|
const initialPartTypeOptions: PartTypeOption[] = [];
|
|
const initialPartUsageOptions: PartUsageOption[] = [];
|
|
const initialGuideRailOptions: GuideRailOption[] = [];
|
|
const initialItemMasterFields: ItemMasterField[] = [];
|
|
const initialItemPages: ItemPage[] = [];
|
|
|
|
// ===== Auth & Cache Setup =====
|
|
const { currentUser } = useAuth();
|
|
const tenantId = currentUser?.tenant?.id;
|
|
|
|
// ✅ TenantAwareCache 인스턴스 생성 (tenant.id 기반, SSR-safe)
|
|
const cache = useMemo(() => {
|
|
// 서버 환경에서는 null 반환 (sessionStorage 없음)
|
|
if (typeof window === 'undefined') return null;
|
|
|
|
// 클라이언트 환경에서만 캐시 생성
|
|
return tenantId ? new TenantAwareCache(tenantId, sessionStorage, 3600000) : null;
|
|
}, [tenantId]);
|
|
|
|
// ===== State Management (SSR-safe) =====
|
|
const [itemMasters, setItemMasters] = useState<ItemMaster[]>(initialItemMasters);
|
|
const [specificationMasters, setSpecificationMasters] = useState<SpecificationMaster[]>(initialSpecificationMasters);
|
|
const [materialItemNames, setMaterialItemNames] = useState<MaterialItemName[]>(initialMaterialItemNames);
|
|
const [itemCategories, setItemCategories] = useState<ItemCategory[]>(initialItemCategories);
|
|
const [itemUnits, setItemUnits] = useState<ItemUnit[]>(initialItemUnits);
|
|
const [itemMaterials, setItemMaterials] = useState<ItemMaterial[]>(initialItemMaterials);
|
|
const [surfaceTreatments, setSurfaceTreatments] = useState<SurfaceTreatment[]>(initialSurfaceTreatments);
|
|
const [partTypeOptions, setPartTypeOptions] = useState<PartTypeOption[]>(initialPartTypeOptions);
|
|
const [partUsageOptions, setPartUsageOptions] = useState<PartUsageOption[]>(initialPartUsageOptions);
|
|
const [guideRailOptions, setGuideRailOptions] = useState<GuideRailOption[]>(initialGuideRailOptions);
|
|
const [sectionTemplates, setSectionTemplates] = useState<SectionTemplate[]>([]);
|
|
const [itemMasterFields, setItemMasterFields] = useState<ItemMasterField[]>(initialItemMasterFields);
|
|
const [itemPages, setItemPages] = useState<ItemPage[]>(initialItemPages);
|
|
|
|
// 2025-11-26 추가: 독립 엔티티 상태
|
|
const [independentSections, setIndependentSections] = useState<ItemSection[]>([]);
|
|
const [independentFields, setIndependentFields] = useState<ItemField[]>([]);
|
|
const [independentBomItems, setIndependentBomItems] = useState<BOMItem[]>([]);
|
|
|
|
// ✅ TenantAwareCache에서 초기 데이터 로드
|
|
useEffect(() => {
|
|
if (!cache) return; // tenant.id 없으면 초기 데이터 유지
|
|
|
|
// ItemMasters
|
|
const cachedItemMasters = cache.get<ItemMaster[]>('itemMasters');
|
|
if (cachedItemMasters) setItemMasters(cachedItemMasters);
|
|
|
|
// SpecificationMasters (버전 체크)
|
|
if (cache.isVersionMatch('specificationMasters', '1.0')) {
|
|
const cachedSpecs = cache.get<SpecificationMaster[]>('specificationMasters');
|
|
if (cachedSpecs) setSpecificationMasters(cachedSpecs);
|
|
}
|
|
|
|
// MaterialItemNames (버전 체크)
|
|
if (cache.isVersionMatch('materialItemNames', '1.1')) {
|
|
const cachedMaterials = cache.get<MaterialItemName[]>('materialItemNames');
|
|
if (cachedMaterials) setMaterialItemNames(cachedMaterials);
|
|
}
|
|
|
|
// ItemCategories
|
|
const cachedCategories = cache.get<ItemCategory[]>('itemCategories');
|
|
if (cachedCategories) setItemCategories(cachedCategories);
|
|
|
|
// ItemUnits
|
|
const cachedUnits = cache.get<ItemUnit[]>('itemUnits');
|
|
if (cachedUnits) setItemUnits(cachedUnits);
|
|
|
|
// ItemMaterials
|
|
const cachedMaterials = cache.get<ItemMaterial[]>('itemMaterials');
|
|
if (cachedMaterials) setItemMaterials(cachedMaterials);
|
|
|
|
// SurfaceTreatments
|
|
const cachedTreatments = cache.get<SurfaceTreatment[]>('surfaceTreatments');
|
|
if (cachedTreatments) setSurfaceTreatments(cachedTreatments);
|
|
|
|
// PartTypeOptions
|
|
const cachedPartTypes = cache.get<PartTypeOption[]>('partTypeOptions');
|
|
if (cachedPartTypes) setPartTypeOptions(cachedPartTypes);
|
|
|
|
// PartUsageOptions
|
|
const cachedPartUsages = cache.get<PartUsageOption[]>('partUsageOptions');
|
|
if (cachedPartUsages) setPartUsageOptions(cachedPartUsages);
|
|
|
|
// GuideRailOptions
|
|
const cachedGuideRails = cache.get<GuideRailOption[]>('guideRailOptions');
|
|
if (cachedGuideRails) setGuideRailOptions(cachedGuideRails);
|
|
|
|
// SectionTemplates
|
|
const cachedTemplates = cache.get<SectionTemplate[]>('sectionTemplates');
|
|
if (cachedTemplates) setSectionTemplates(cachedTemplates);
|
|
|
|
// ItemMasterFields
|
|
const cachedFields = cache.get<ItemMasterField[]>('itemMasterFields');
|
|
if (cachedFields) setItemMasterFields(cachedFields);
|
|
|
|
// ItemPages
|
|
const cachedPages = cache.get<ItemPage[]>('itemPages');
|
|
if (cachedPages) setItemPages(cachedPages);
|
|
}, [cache]);
|
|
|
|
// ✅ TenantAwareCache 동기화 (상태 변경 시 자동 저장)
|
|
useEffect(() => {
|
|
if (cache && itemMasters !== initialItemMasters) {
|
|
cache.set('itemMasters', itemMasters);
|
|
}
|
|
}, [cache, itemMasters]);
|
|
|
|
useEffect(() => {
|
|
if (cache && specificationMasters !== initialSpecificationMasters) {
|
|
cache.set('specificationMasters', specificationMasters, '1.0');
|
|
}
|
|
}, [cache, specificationMasters]);
|
|
|
|
useEffect(() => {
|
|
if (cache && materialItemNames !== initialMaterialItemNames) {
|
|
cache.set('materialItemNames', materialItemNames, '1.1');
|
|
}
|
|
}, [cache, materialItemNames]);
|
|
|
|
useEffect(() => {
|
|
if (cache && itemCategories !== initialItemCategories) {
|
|
cache.set('itemCategories', itemCategories);
|
|
}
|
|
}, [cache, itemCategories]);
|
|
|
|
useEffect(() => {
|
|
if (cache && itemUnits !== initialItemUnits) {
|
|
cache.set('itemUnits', itemUnits);
|
|
}
|
|
}, [cache, itemUnits]);
|
|
|
|
useEffect(() => {
|
|
if (cache && itemMaterials !== initialItemMaterials) {
|
|
cache.set('itemMaterials', itemMaterials);
|
|
}
|
|
}, [cache, itemMaterials]);
|
|
|
|
useEffect(() => {
|
|
if (cache && surfaceTreatments !== initialSurfaceTreatments) {
|
|
cache.set('surfaceTreatments', surfaceTreatments);
|
|
}
|
|
}, [cache, surfaceTreatments]);
|
|
|
|
useEffect(() => {
|
|
if (cache && partTypeOptions !== initialPartTypeOptions) {
|
|
cache.set('partTypeOptions', partTypeOptions);
|
|
}
|
|
}, [cache, partTypeOptions]);
|
|
|
|
useEffect(() => {
|
|
if (cache && partUsageOptions !== initialPartUsageOptions) {
|
|
cache.set('partUsageOptions', partUsageOptions);
|
|
}
|
|
}, [cache, partUsageOptions]);
|
|
|
|
useEffect(() => {
|
|
if (cache && guideRailOptions !== initialGuideRailOptions) {
|
|
cache.set('guideRailOptions', guideRailOptions);
|
|
}
|
|
}, [cache, guideRailOptions]);
|
|
|
|
useEffect(() => {
|
|
if (cache) {
|
|
cache.set('sectionTemplates', sectionTemplates);
|
|
}
|
|
}, [cache, sectionTemplates]);
|
|
|
|
useEffect(() => {
|
|
if (cache && itemMasterFields !== initialItemMasterFields) {
|
|
cache.set('itemMasterFields', itemMasterFields);
|
|
}
|
|
}, [cache, itemMasterFields]);
|
|
|
|
useEffect(() => {
|
|
if (cache && itemPages !== initialItemPages) {
|
|
cache.set('itemPages', itemPages);
|
|
}
|
|
}, [cache, itemPages]);
|
|
|
|
// ===== CRUD Functions =====
|
|
|
|
// ItemMaster CRUD
|
|
const addItemMaster = (item: ItemMaster) => {
|
|
setItemMasters(prev => [...prev, item]);
|
|
};
|
|
|
|
const updateItemMaster = (id: string, updates: Partial<ItemMaster>) => {
|
|
setItemMasters(prev => prev.map(item => item.id === id ? { ...item, ...updates } : item));
|
|
};
|
|
|
|
const deleteItemMaster = (id: string) => {
|
|
setItemMasters(prev => prev.filter(item => item.id !== id));
|
|
};
|
|
|
|
// SpecificationMaster CRUD
|
|
const addSpecificationMaster = (spec: SpecificationMaster) => {
|
|
setSpecificationMasters(prev => [...prev, spec]);
|
|
};
|
|
|
|
const updateSpecificationMaster = (id: string, updates: Partial<SpecificationMaster>) => {
|
|
setSpecificationMasters(prev => prev.map(spec => spec.id === id ? { ...spec, ...updates } : spec));
|
|
};
|
|
|
|
const deleteSpecificationMaster = (id: string) => {
|
|
setSpecificationMasters(prev => prev.filter(spec => spec.id !== id));
|
|
};
|
|
|
|
// MaterialItemName CRUD
|
|
const addMaterialItemName = (item: MaterialItemName) => {
|
|
setMaterialItemNames(prev => [...prev, item]);
|
|
};
|
|
|
|
const updateMaterialItemName = (id: string, updates: Partial<MaterialItemName>) => {
|
|
setMaterialItemNames(prev => prev.map(item => item.id === id ? { ...item, ...updates } : item));
|
|
};
|
|
|
|
const deleteMaterialItemName = (id: string) => {
|
|
setMaterialItemNames(prev => prev.filter(item => item.id !== id));
|
|
};
|
|
|
|
// ItemCategory CRUD
|
|
const addItemCategory = (category: ItemCategory) => {
|
|
setItemCategories(prev => [...prev, category]);
|
|
};
|
|
|
|
const updateItemCategory = (id: string, updates: Partial<ItemCategory>) => {
|
|
setItemCategories(prev => prev.map(cat => cat.id === id ? { ...cat, ...updates } : cat));
|
|
};
|
|
|
|
const deleteItemCategory = (id: string) => {
|
|
setItemCategories(prev => prev.filter(cat => cat.id !== id));
|
|
};
|
|
|
|
// ItemUnit CRUD
|
|
const addItemUnit = (unit: ItemUnit) => {
|
|
setItemUnits(prev => [...prev, unit]);
|
|
};
|
|
|
|
const updateItemUnit = (id: string, updates: Partial<ItemUnit>) => {
|
|
setItemUnits(prev => prev.map(unit => unit.id === id ? { ...unit, ...updates } : unit));
|
|
};
|
|
|
|
const deleteItemUnit = (id: string) => {
|
|
setItemUnits(prev => prev.filter(unit => unit.id !== id));
|
|
};
|
|
|
|
// ItemMaterial CRUD
|
|
const addItemMaterial = (material: ItemMaterial) => {
|
|
setItemMaterials(prev => [...prev, material]);
|
|
};
|
|
|
|
const updateItemMaterial = (id: string, updates: Partial<ItemMaterial>) => {
|
|
setItemMaterials(prev => prev.map(mat => mat.id === id ? { ...mat, ...updates } : mat));
|
|
};
|
|
|
|
const deleteItemMaterial = (id: string) => {
|
|
setItemMaterials(prev => prev.filter(mat => mat.id !== id));
|
|
};
|
|
|
|
// SurfaceTreatment CRUD
|
|
const addSurfaceTreatment = (treatment: SurfaceTreatment) => {
|
|
setSurfaceTreatments(prev => [...prev, treatment]);
|
|
};
|
|
|
|
const updateSurfaceTreatment = (id: string, updates: Partial<SurfaceTreatment>) => {
|
|
setSurfaceTreatments(prev => prev.map(treat => treat.id === id ? { ...treat, ...updates } : treat));
|
|
};
|
|
|
|
const deleteSurfaceTreatment = (id: string) => {
|
|
setSurfaceTreatments(prev => prev.filter(treat => treat.id !== id));
|
|
};
|
|
|
|
// PartTypeOption CRUD
|
|
const addPartTypeOption = (option: PartTypeOption) => {
|
|
setPartTypeOptions(prev => [...prev, option]);
|
|
};
|
|
|
|
const updatePartTypeOption = (id: string, updates: Partial<PartTypeOption>) => {
|
|
setPartTypeOptions(prev => prev.map(opt => opt.id === id ? { ...opt, ...updates } : opt));
|
|
};
|
|
|
|
const deletePartTypeOption = (id: string) => {
|
|
setPartTypeOptions(prev => prev.filter(opt => opt.id !== id));
|
|
};
|
|
|
|
// PartUsageOption CRUD
|
|
const addPartUsageOption = (option: PartUsageOption) => {
|
|
setPartUsageOptions(prev => [...prev, option]);
|
|
};
|
|
|
|
const updatePartUsageOption = (id: string, updates: Partial<PartUsageOption>) => {
|
|
setPartUsageOptions(prev => prev.map(opt => opt.id === id ? { ...opt, ...updates } : opt));
|
|
};
|
|
|
|
const deletePartUsageOption = (id: string) => {
|
|
setPartUsageOptions(prev => prev.filter(opt => opt.id !== id));
|
|
};
|
|
|
|
// GuideRailOption CRUD
|
|
const addGuideRailOption = (option: GuideRailOption) => {
|
|
setGuideRailOptions(prev => [...prev, option]);
|
|
};
|
|
|
|
const updateGuideRailOption = (id: string, updates: Partial<GuideRailOption>) => {
|
|
setGuideRailOptions(prev => prev.map(opt => opt.id === id ? { ...opt, ...updates } : opt));
|
|
};
|
|
|
|
const deleteGuideRailOption = (id: string) => {
|
|
setGuideRailOptions(prev => prev.filter(opt => opt.id !== id));
|
|
};
|
|
|
|
// ItemMasterField CRUD (임시: 로컬 state)
|
|
/**
|
|
* 초기 데이터 로딩용: API 호출 없이 마스터 필드를 state에 로드 (덮어쓰기)
|
|
*/
|
|
/**
|
|
* @deprecated 2025-11-27: item_fields로 통합됨.
|
|
* independentFields 및 loadIndependentFields 사용 권장
|
|
*/
|
|
const loadItemMasterFields = (fields: ItemMasterField[]) => {
|
|
setItemMasterFields(fields);
|
|
console.log('[ItemMasterContext] 마스터 필드 로드 완료:', fields.length);
|
|
};
|
|
|
|
/**
|
|
* @deprecated 2025-11-27: item_fields로 통합됨.
|
|
* independentFields 및 createIndependentField 사용 권장
|
|
*/
|
|
const addItemMasterField = async (field: Omit<ItemMasterField, 'id' | 'created_at' | 'updated_at'>) => {
|
|
try {
|
|
// API 호출 - 2025-11-27: fields.createIndependent() 사용 (masterFields.create() deprecated)
|
|
// Note: API가 ItemFieldResponse를 직접 반환 (wrapper 없음)
|
|
// 2025-11-28: field_key 추가
|
|
const response = await itemMasterApi.fields.createIndependent({
|
|
field_name: field.field_name,
|
|
field_key: field.field_key ?? undefined, // 2025-11-28: field_key 추가
|
|
field_type: field.field_type,
|
|
category: field.category ?? undefined,
|
|
description: field.description ?? undefined,
|
|
is_common: field.is_common,
|
|
default_value: field.default_value ?? undefined,
|
|
options: field.options ?? undefined,
|
|
validation_rules: field.validation_rules ?? undefined,
|
|
properties: field.properties ?? undefined,
|
|
});
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
// 2025-11-27: API가 ItemFieldResponse를 직접 반환하므로 response를 직접 사용
|
|
// 2025-11-28: field_key 추가
|
|
const newField: ItemMasterField = {
|
|
id: response.id,
|
|
tenant_id: response.tenant_id,
|
|
field_name: response.field_name,
|
|
field_key: response.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.field_type,
|
|
category: response.category,
|
|
description: response.description,
|
|
is_common: response.is_common,
|
|
default_value: response.default_value,
|
|
options: response.options,
|
|
validation_rules: response.validation_rules,
|
|
properties: response.properties,
|
|
created_by: response.created_by,
|
|
updated_by: response.updated_by,
|
|
created_at: response.created_at,
|
|
updated_at: response.updated_at,
|
|
};
|
|
|
|
setItemMasterFields(prev => [...prev, newField]);
|
|
console.log('[ItemMasterContext] 마스터 필드 생성 성공:', newField.id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 마스터 필드 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @deprecated 2025-11-27: item_fields로 통합됨.
|
|
* independentFields 및 updateIndependentField 사용 권장
|
|
*/
|
|
const updateItemMasterField = async (id: number, updates: Partial<ItemMasterField>) => {
|
|
try {
|
|
// API 호출 - 2025-11-27: fields.update() 사용 (masterFields.update() deprecated)
|
|
const requestData: any = {};
|
|
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
|
if (updates.field_key !== undefined) requestData.field_key = updates.field_key; // 2025-11-28: field_key 추가
|
|
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
|
if (updates.category !== undefined) requestData.category = updates.category;
|
|
if (updates.description !== undefined) requestData.description = updates.description;
|
|
if (updates.is_common !== undefined) requestData.is_common = updates.is_common;
|
|
if (updates.default_value !== undefined) requestData.default_value = updates.default_value;
|
|
if (updates.options !== undefined) requestData.options = updates.options;
|
|
if (updates.validation_rules !== undefined) requestData.validation_rules = updates.validation_rules;
|
|
if (updates.properties !== undefined) requestData.properties = updates.properties;
|
|
|
|
const response = await itemMasterApi.fields.update(id, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '마스터 필드 수정 실패');
|
|
}
|
|
|
|
// state 업데이트 - 2025-11-28: field_key 추가
|
|
setItemMasterFields(prev => prev.map(field =>
|
|
field.id === id ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
category: response.data!.category,
|
|
description: response.data!.description,
|
|
is_common: response.data!.is_common,
|
|
default_value: response.data!.default_value,
|
|
options: response.data!.options,
|
|
validation_rules: response.data!.validation_rules,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
));
|
|
|
|
// 2025-11-27: 섹션탭/계층구조 실시간 반영
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
// 2025-11-28: field_key 추가
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).map(field =>
|
|
field.id === id ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value,
|
|
options: response.data!.options,
|
|
validation_rules: response.data!.validation_rules,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
)
|
|
})));
|
|
|
|
// 2025-11-28: field_key 추가
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).map(field =>
|
|
field.id === id ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value,
|
|
options: response.data!.options,
|
|
validation_rules: response.data!.validation_rules,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
)
|
|
}))
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 마스터 필드 수정 성공:', id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 마스터 필드 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @deprecated 2025-11-27: item_fields로 통합됨.
|
|
* independentFields 및 deleteIndependentField 사용 권장
|
|
*/
|
|
const deleteItemMasterField = async (id: number) => {
|
|
try {
|
|
// API 호출 - 2025-11-27: fields.delete() 사용 (masterFields.delete() deprecated)
|
|
const response = await itemMasterApi.fields.delete(id);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '마스터 필드 삭제 실패');
|
|
}
|
|
|
|
// state 업데이트
|
|
setItemMasterFields(prev => prev.filter(field => field.id !== id));
|
|
|
|
// 2025-11-27: 섹션탭/계층구조 실시간 반영
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).filter(field => field.id !== id)
|
|
})));
|
|
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).filter(field => field.id !== id)
|
|
}))
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 마스터 필드 삭제 성공:', id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 마스터 필드 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// SectionTemplate CRUD with API
|
|
/**
|
|
* 초기 데이터 로딩용: API 호출 없이 섹션 템플릿을 state에 로드 (덮어쓰기)
|
|
*/
|
|
const loadSectionTemplates = (templates: SectionTemplate[]) => {
|
|
setSectionTemplates(templates);
|
|
console.log('[ItemMasterContext] 섹션 템플릿 로드 완료:', templates.length);
|
|
};
|
|
|
|
const addSectionTemplate = async (template: Omit<SectionTemplate, 'id' | 'created_at' | 'updated_at'>) => {
|
|
try {
|
|
// 프론트엔드 형식 → API 형식 변환
|
|
// template_name → title, section_type → type
|
|
const apiType = template.section_type === 'BOM' ? 'bom' : 'fields';
|
|
|
|
const response = await itemMasterApi.templates.create({
|
|
title: template.template_name,
|
|
type: apiType,
|
|
description: template.description ?? undefined,
|
|
is_default: false, // 기본값
|
|
});
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '섹션 템플릿 생성 실패');
|
|
}
|
|
|
|
// API 응답 → 프론트엔드 형식 변환
|
|
// title → template_name, type → section_type
|
|
const SECTION_TYPE_MAP: Record<string, 'BASIC' | 'BOM' | 'CUSTOM'> = {
|
|
fields: 'BASIC',
|
|
bom: 'BOM',
|
|
};
|
|
|
|
const newTemplate: SectionTemplate = {
|
|
id: response.data.id,
|
|
tenant_id: response.data.tenant_id,
|
|
template_name: response.data.title,
|
|
section_type: SECTION_TYPE_MAP[response.data.type] || 'BASIC',
|
|
description: response.data.description,
|
|
default_fields: null,
|
|
category: template.category,
|
|
fields: template.fields,
|
|
bomItems: template.bomItems,
|
|
created_by: response.data.created_by,
|
|
updated_by: response.data.updated_by,
|
|
created_at: response.data.created_at,
|
|
updated_at: response.data.updated_at,
|
|
};
|
|
|
|
setSectionTemplates(prev => [...prev, newTemplate]);
|
|
console.log('[ItemMasterContext] 섹션 템플릿 생성 성공:', newTemplate.id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 템플릿 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const updateSectionTemplate = async (id: number, updates: Partial<SectionTemplate>) => {
|
|
try {
|
|
// default_fields, fields, category, bomItems는 로컬에서만 관리 (API 미지원)
|
|
const localOnlyUpdates = ['default_fields', 'fields', 'category', 'bomItems'];
|
|
const hasApiUpdates = Object.keys(updates).some(key => !localOnlyUpdates.includes(key));
|
|
const hasLocalUpdates = Object.keys(updates).some(key => localOnlyUpdates.includes(key));
|
|
|
|
// API 호출이 필요한 경우에만 API 요청
|
|
if (hasApiUpdates) {
|
|
// API 요청 형식으로 변환 (frontend → API)
|
|
// frontend: template_name, section_type
|
|
// API: title, type
|
|
const requestData: any = {};
|
|
if (updates.template_name !== undefined) requestData.title = updates.template_name;
|
|
if (updates.section_type !== undefined) {
|
|
// section_type 변환: 'BASIC' | 'CUSTOM' → 'fields', 'BOM' → 'bom'
|
|
requestData.type = updates.section_type === 'BOM' ? 'bom' : 'fields';
|
|
}
|
|
if (updates.description !== undefined) requestData.description = updates.description;
|
|
|
|
const response = await itemMasterApi.templates.update(id, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '섹션 템플릿 수정 실패');
|
|
}
|
|
|
|
// state 업데이트 (API 응답 → frontend 형식으로 변환)
|
|
// API 응답: title, type ('fields' | 'bom')
|
|
// Frontend 형식: template_name, section_type ('BASIC' | 'BOM' | 'CUSTOM')
|
|
const SECTION_TYPE_MAP: Record<string, 'BASIC' | 'BOM' | 'CUSTOM'> = {
|
|
fields: 'BASIC',
|
|
bom: 'BOM',
|
|
};
|
|
|
|
// 1. sectionTemplates 업데이트 (섹션 탭)
|
|
setSectionTemplates(prev => prev.map(template =>
|
|
template.id === id ? {
|
|
...template,
|
|
template_name: response.data!.title,
|
|
section_type: SECTION_TYPE_MAP[response.data!.type] || 'BASIC',
|
|
description: response.data!.description,
|
|
updated_at: response.data!.updated_at,
|
|
} : template
|
|
));
|
|
|
|
// 2. itemPages 업데이트 (계층구조 탭) - 같은 ID의 섹션도 동기화
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === id
|
|
? { ...section, title: response.data!.title }
|
|
: section
|
|
)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 섹션 템플릿 수정 성공 (양방향 동기화):', id);
|
|
}
|
|
|
|
// 로컬 전용 필드 업데이트 (default_fields, fields, category, bomItems)
|
|
if (hasLocalUpdates) {
|
|
setSectionTemplates(prev => prev.map(template => {
|
|
if (template.id !== id) return template;
|
|
|
|
const updatedTemplate = { ...template };
|
|
|
|
// default_fields 업데이트 시 fields도 같이 업데이트
|
|
if (updates.default_fields !== undefined) {
|
|
updatedTemplate.default_fields = updates.default_fields;
|
|
updatedTemplate.fields = updates.default_fields as TemplateField[];
|
|
}
|
|
if (updates.fields !== undefined) {
|
|
updatedTemplate.fields = updates.fields;
|
|
updatedTemplate.default_fields = updates.fields;
|
|
}
|
|
if (updates.category !== undefined) {
|
|
updatedTemplate.category = updates.category;
|
|
}
|
|
if (updates.bomItems !== undefined) {
|
|
updatedTemplate.bomItems = updates.bomItems;
|
|
}
|
|
|
|
return updatedTemplate;
|
|
}));
|
|
|
|
console.log('[ItemMasterContext] 섹션 템플릿 수정 성공 (로컬):', id, Object.keys(updates).filter(k => localOnlyUpdates.includes(k)));
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 템플릿 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteSectionTemplate = async (id: number) => {
|
|
try {
|
|
// API 호출
|
|
const response = await itemMasterApi.templates.delete(id);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '섹션 템플릿 삭제 실패');
|
|
}
|
|
|
|
// 1. sectionTemplates 업데이트 (섹션 탭)
|
|
setSectionTemplates(prev => prev.filter(template => template.id !== id));
|
|
|
|
// 2. itemPages 업데이트 (계층구조 탭) - 같은 ID의 섹션도 삭제
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.filter(section => section.id !== id)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 섹션 템플릿 삭제 성공 (양방향 동기화):', id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 템플릿 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ItemPage CRUD with API
|
|
|
|
/**
|
|
* 초기 데이터 로딩용: API 호출 없이 페이지 데이터를 state에 로드 (덮어쓰기)
|
|
* (이미 백엔드에서 받아온 데이터를 로드할 때 사용)
|
|
*/
|
|
const loadItemPages = (pages: ItemPage[]) => {
|
|
setItemPages(pages); // 덮어쓰기 (append가 아님!)
|
|
console.log('[ItemMasterContext] 페이지 데이터 로드 완료:', pages.length);
|
|
};
|
|
|
|
/**
|
|
* 새 페이지 생성: API 호출 + state 업데이트
|
|
* (사용자가 새 페이지를 만들 때 사용)
|
|
* @returns 생성된 페이지 반환
|
|
*/
|
|
const addItemPage = async (pageData: Omit<ItemPage, 'id' | 'created_at' | 'updated_at'>): Promise<ItemPage> => {
|
|
try {
|
|
// API 요청 데이터 변환
|
|
const requestData: ItemPageRequest = {
|
|
page_name: pageData.page_name,
|
|
item_type: pageData.item_type,
|
|
absolute_path: pageData.absolute_path || '',
|
|
};
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.pages.create(requestData);
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
const newPage = transformPageResponse(response);
|
|
setItemPages(prev => [...prev, newPage]);
|
|
|
|
console.log('[ItemMasterContext] 페이지 생성 성공:', newPage);
|
|
return newPage;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 페이지 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const updateItemPage = async (id: number, updates: Partial<ItemPage>) => {
|
|
try {
|
|
// API 요청 데이터 변환
|
|
const requestData: Partial<ItemPageRequest> = {};
|
|
if (updates.page_name) requestData.page_name = updates.page_name;
|
|
if (updates.absolute_path !== undefined) requestData.absolute_path = updates.absolute_path;
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.pages.update(id, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '페이지 수정 실패');
|
|
}
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
const updatedPage = transformPageResponse(response.data);
|
|
setItemPages(prev => prev.map(page => page.id === id ? updatedPage : page));
|
|
|
|
console.log('[ItemMasterContext] 페이지 수정 성공:', updatedPage);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 페이지 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteItemPage = async (id: number) => {
|
|
try {
|
|
// 2025-12-01: 페이지 삭제 전에 해당 페이지의 섹션들(필드 포함)을 저장
|
|
// refreshIndependentSections()는 백엔드에서 섹션만 가져오고 필드 데이터는 포함하지 않음
|
|
// 따라서 직접 섹션 데이터를 보존하여 필드 연결을 유지해야 함
|
|
const pageToDelete = itemPages.find(page => page.id === id);
|
|
const sectionsToPreserve = pageToDelete?.sections || [];
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.pages.delete(id);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '페이지 삭제 실패');
|
|
}
|
|
|
|
// state 업데이트
|
|
setItemPages(prev => prev.filter(page => page.id !== id));
|
|
|
|
// 2025-12-01: 페이지의 섹션들을 독립 섹션으로 이동 (필드 데이터 유지)
|
|
// refreshIndependentSections() 대신 직접 섹션 추가하여 필드 연결 보존
|
|
if (sectionsToPreserve.length > 0) {
|
|
setIndependentSections(prev => {
|
|
// 기존 독립 섹션 ID 목록
|
|
const existingIds = new Set(prev.map(s => s.id));
|
|
// 중복되지 않는 섹션만 추가 (필드 포함된 원본 데이터)
|
|
const newSections = sectionsToPreserve.filter(s => !existingIds.has(s.id));
|
|
console.log('[ItemMasterContext] 페이지 삭제 - 섹션을 독립 섹션으로 이동:', {
|
|
preserved: newSections.length,
|
|
withFields: newSections.map(s => ({
|
|
id: s.id,
|
|
title: s.title,
|
|
fieldCount: s.fields?.length || 0
|
|
}))
|
|
});
|
|
return [...prev, ...newSections];
|
|
});
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 페이지 삭제 성공:', id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 페이지 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const reorderPages = async (newOrder: Array<{ id: number; order_no: number }>) => {
|
|
try {
|
|
// Optimistic UI 업데이트 (즉시 반영)
|
|
setItemPages(prev => {
|
|
const updated = [...prev];
|
|
updated.sort((a, b) => {
|
|
const orderA = newOrder.find(o => o.id === a.id)?.order_no ?? 0;
|
|
const orderB = newOrder.find(o => o.id === b.id)?.order_no ?? 0;
|
|
return orderA - orderB;
|
|
});
|
|
return updated.map(page => {
|
|
const newOrderNo = newOrder.find(o => o.id === page.id)?.order_no;
|
|
return newOrderNo !== undefined ? { ...page, order_no: newOrderNo } : page;
|
|
});
|
|
});
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.pages.reorder({ page_orders: newOrder });
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '페이지 순서 변경 실패');
|
|
}
|
|
|
|
// API 응답으로 최종 업데이트
|
|
const reorderedPages = response.data.map(transformPageResponse);
|
|
setItemPages(reorderedPages);
|
|
|
|
console.log('[ItemMasterContext] 페이지 순서 변경 성공');
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 페이지 순서 변경 실패:', errorMessage);
|
|
|
|
// 실패 시 이전 상태로 롤백
|
|
// 여기서는 페이지 전체를 다시 로드하는 것이 더 안전할 수 있음
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Section CRUD with API
|
|
const addSectionToPage = async (pageId: number, sectionData: Omit<ItemSection, 'id' | 'created_at' | 'updated_at'>) => {
|
|
try {
|
|
// API 호출
|
|
const response = await itemMasterApi.sections.create(pageId, {
|
|
title: sectionData.title,
|
|
type: sectionData.section_type === 'BOM' ? 'bom' : 'fields',
|
|
});
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '섹션 생성 실패');
|
|
}
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
const newSection = transformSectionResponse(response.data);
|
|
setItemPages(prev => prev.map(page =>
|
|
page.id === pageId
|
|
? { ...page, sections: [...page.sections, newSection] }
|
|
: page
|
|
));
|
|
|
|
console.log('[ItemMasterContext] 섹션 생성 성공:', newSection);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const updateSection = async (sectionId: number, updates: Partial<ItemSection>) => {
|
|
try {
|
|
// API 요청 데이터 변환
|
|
const requestData: any = {};
|
|
if (updates.title) requestData.title = updates.title;
|
|
// page_id 변경 지원 (연결 해제 시 null)
|
|
if ('page_id' in updates) requestData.page_id = updates.page_id;
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.sections.update(sectionId, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '섹션 수정 실패');
|
|
}
|
|
|
|
// 응답 데이터 변환
|
|
const updatedSection = transformSectionResponse(response.data);
|
|
|
|
// page_id가 null로 변경되면 (연결 해제) 페이지에서 섹션 제거 + 독립 섹션에 추가
|
|
if (updates.page_id === null) {
|
|
// 1. itemPages에서 해당 섹션 찾아서 제거
|
|
let unlinkedSection: ItemSection | null = null;
|
|
setItemPages(prev => prev.map(page => {
|
|
const foundSection = page.sections.find(s => s.id === sectionId);
|
|
if (foundSection) {
|
|
unlinkedSection = { ...foundSection, page_id: null };
|
|
}
|
|
return {
|
|
...page,
|
|
sections: page.sections.filter(section => section.id !== sectionId)
|
|
};
|
|
}));
|
|
|
|
// 2. 독립 섹션에 추가 (섹션 탭에서 보이도록)
|
|
if (unlinkedSection) {
|
|
setIndependentSections(prev => [...prev, unlinkedSection!]);
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 섹션 연결 해제 (계층구조→독립):', sectionId);
|
|
} else {
|
|
// 일반 수정: 섹션 데이터 업데이트
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === sectionId ? updatedSection : section
|
|
)
|
|
})));
|
|
}
|
|
|
|
// 2. sectionTemplates 업데이트 (섹션 탭) - 같은 ID의 템플릿도 동기화
|
|
setSectionTemplates(prev => prev.map(template =>
|
|
template.id === sectionId
|
|
? { ...template, template_name: updatedSection.title }
|
|
: template
|
|
));
|
|
|
|
// 3. independentSections 업데이트 (독립 섹션) - sectionsAsTemplates useMemo 재계산 트리거
|
|
setIndependentSections(prev => prev.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, ...updatedSection }
|
|
: section
|
|
));
|
|
|
|
console.log('[ItemMasterContext] 섹션 수정 성공 (3방향 동기화):', updatedSection);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteSection = async (sectionId: number) => {
|
|
try {
|
|
// API 호출
|
|
const response = await itemMasterApi.sections.delete(sectionId);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '섹션 삭제 실패');
|
|
}
|
|
|
|
// 1. itemPages 업데이트 (계층구조 탭)
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.filter(section => section.id !== sectionId)
|
|
})));
|
|
|
|
// 2. sectionTemplates 업데이트 (섹션 탭) - 같은 ID의 템플릿도 삭제
|
|
setSectionTemplates(prev => prev.filter(template => template.id !== sectionId));
|
|
|
|
// 3. independentSections 업데이트 (독립 섹션) - sectionsAsTemplates useMemo 재계산 트리거
|
|
setIndependentSections(prev => prev.filter(section => section.id !== sectionId));
|
|
|
|
console.log('[ItemMasterContext] 섹션 삭제 성공 (3방향 동기화):', sectionId);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const reorderSections = async (pageId: number, sectionIds: number[]) => {
|
|
try {
|
|
// 요청 데이터 구성 (백엔드는 'items' 필드를 기대함)
|
|
const requestData = {
|
|
items: sectionIds.map((id, index) => ({ id, order_no: index + 1 })) // order_no는 1부터 시작
|
|
};
|
|
console.log('[ItemMasterContext] 섹션 순서 변경 요청:', {
|
|
pageId,
|
|
sectionIds,
|
|
requestData
|
|
});
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.sections.reorder(pageId, requestData);
|
|
console.log('[ItemMasterContext] 섹션 순서 변경 응답:', response);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '섹션 순서 변경 실패');
|
|
}
|
|
|
|
// 응답 데이터 처리 - 배열 또는 객체 형태 모두 지원
|
|
// 백엔드 응답 형식에 따라 처리
|
|
if (response.data && Array.isArray(response.data)) {
|
|
// 배열 형태: 변경된 섹션 목록 반환
|
|
const reorderedSections = response.data.map(transformSectionResponse);
|
|
setItemPages(prev => prev.map(page =>
|
|
page.id === pageId
|
|
? { ...page, sections: reorderedSections }
|
|
: page
|
|
));
|
|
} else {
|
|
// 배열이 아닌 경우: 로컬 state에서 순서만 업데이트
|
|
setItemPages(prev => prev.map(page => {
|
|
if (page.id !== pageId) return page;
|
|
|
|
// sectionIds 순서대로 섹션 재정렬
|
|
const sectionMap = new Map(page.sections.map(s => [s.id, s]));
|
|
const reorderedSections = sectionIds
|
|
.map((id, index) => {
|
|
const section = sectionMap.get(id);
|
|
return section ? { ...section, order_no: index + 1 } : null;
|
|
})
|
|
.filter((s): s is ItemSection => s !== null);
|
|
|
|
return { ...page, sections: reorderedSections };
|
|
}));
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 섹션 순서 변경 성공');
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 순서 변경 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Field CRUD with API
|
|
const addFieldToSection = async (sectionId: number, fieldData: Omit<ItemField, 'id' | 'created_at' | 'updated_at'>) => {
|
|
try {
|
|
// API 호출 (null → undefined 변환)
|
|
// 2025-11-28: field_key 추가
|
|
const response = await itemMasterApi.fields.create(sectionId, {
|
|
field_name: fieldData.field_name,
|
|
field_key: fieldData.field_key ?? undefined, // 2025-11-28: field_key 추가
|
|
field_type: fieldData.field_type,
|
|
is_required: fieldData.is_required,
|
|
default_value: fieldData.default_value ?? undefined,
|
|
placeholder: fieldData.placeholder ?? undefined,
|
|
display_condition: fieldData.display_condition ?? undefined,
|
|
validation_rules: fieldData.validation_rules ?? undefined,
|
|
options: fieldData.options ?? undefined,
|
|
properties: fieldData.properties ?? undefined,
|
|
});
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '필드 생성 실패');
|
|
}
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
// 2025-11-28: field_key 추가
|
|
const newField: ItemField = {
|
|
id: response.data.id,
|
|
tenant_id: response.data.tenant_id,
|
|
section_id: response.data.section_id,
|
|
master_field_id: response.data.master_field_id,
|
|
field_name: response.data.field_name,
|
|
field_key: response.data.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data.field_type,
|
|
order_no: response.data.order_no,
|
|
is_required: response.data.is_required,
|
|
default_value: response.data.default_value,
|
|
placeholder: response.data.placeholder,
|
|
display_condition: response.data.display_condition,
|
|
validation_rules: response.data.validation_rules,
|
|
options: response.data.options,
|
|
properties: response.data.properties,
|
|
created_at: response.data.created_at,
|
|
updated_at: response.data.updated_at,
|
|
};
|
|
|
|
console.log('[addFieldToSection] Before setItemPages, sectionId:', sectionId, 'newField:', newField.field_name);
|
|
|
|
// 2025-11-27: itemPages 업데이트 (페이지에 연결된 섹션)
|
|
setItemPages(prev => {
|
|
const updated = prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, fields: [...(section.fields || []), newField] }
|
|
: section
|
|
)
|
|
}));
|
|
console.log('[addFieldToSection] After setItemPages, updated pages:', updated.map(p => ({
|
|
id: p.id,
|
|
sections: p.sections.map(s => ({
|
|
id: s.id,
|
|
title: s.title,
|
|
fieldsCount: s.fields?.length || 0
|
|
}))
|
|
})));
|
|
return updated;
|
|
});
|
|
|
|
// 2025-11-27: independentSections도 업데이트 (독립 섹션에 필드 추가된 경우)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존하므로
|
|
// 두 상태 모두 업데이트해야 모든 탭에서 실시간 동기화됨
|
|
setIndependentSections(prev => {
|
|
const updated = prev.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, fields: [...(section.fields || []), newField] }
|
|
: section
|
|
);
|
|
console.log('[addFieldToSection] After setIndependentSections, updated sections:', updated.map(s => ({
|
|
id: s.id,
|
|
title: s.title,
|
|
fieldsCount: s.fields?.length || 0
|
|
})));
|
|
return updated;
|
|
});
|
|
|
|
// 2025-11-27: 항목탭/속성탭 실시간 동기화
|
|
// ItemField → ItemMasterField 변환하여 추가
|
|
// 2025-11-28: field_key 추가
|
|
const newMasterField: ItemMasterField = {
|
|
id: newField.id,
|
|
tenant_id: newField.tenant_id ?? 0,
|
|
field_name: newField.field_name,
|
|
field_key: newField.field_key, // 2025-11-28: field_key 추가
|
|
field_type: newField.field_type,
|
|
description: newField.placeholder ?? null,
|
|
category: null,
|
|
is_common: false,
|
|
is_required: newField.is_required,
|
|
default_value: newField.default_value ?? null,
|
|
options: newField.options ?? null,
|
|
validation_rules: newField.validation_rules ?? null,
|
|
properties: newField.properties ?? null,
|
|
created_by: null,
|
|
updated_by: null,
|
|
created_at: newField.created_at,
|
|
updated_at: newField.updated_at,
|
|
};
|
|
setItemMasterFields(prev => [...prev, newMasterField]);
|
|
|
|
console.log('[ItemMasterContext] 필드 생성 성공:', newField.id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const updateField = async (fieldId: number, updates: Partial<ItemField>) => {
|
|
try {
|
|
// API 호출 (null → undefined 변환)
|
|
const requestData: any = {};
|
|
if (updates.field_name !== undefined) requestData.field_name = updates.field_name;
|
|
// 2025-11-28: field_key 추가
|
|
if (updates.field_key !== undefined) requestData.field_key = updates.field_key ?? undefined;
|
|
if (updates.field_type !== undefined) requestData.field_type = updates.field_type;
|
|
if (updates.is_required !== undefined) requestData.is_required = updates.is_required;
|
|
if (updates.default_value !== undefined) requestData.default_value = updates.default_value ?? undefined;
|
|
if (updates.placeholder !== undefined) requestData.placeholder = updates.placeholder ?? undefined;
|
|
// 2025-11-27: display_condition도 API로 전송 (JSON 타입으로 저장)
|
|
if (updates.display_condition !== undefined) requestData.display_condition = updates.display_condition ?? undefined;
|
|
if (updates.validation_rules !== undefined) requestData.validation_rules = updates.validation_rules ?? undefined;
|
|
if (updates.options !== undefined) requestData.options = updates.options ?? undefined;
|
|
if (updates.properties !== undefined) requestData.properties = updates.properties ?? undefined;
|
|
|
|
const response = await itemMasterApi.fields.update(fieldId, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || '필드 수정 실패');
|
|
}
|
|
|
|
// state 업데이트
|
|
// 2025-11-28: field_key 추가
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).map(field =>
|
|
field.id === fieldId ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value,
|
|
placeholder: response.data!.placeholder,
|
|
display_condition: response.data!.display_condition,
|
|
validation_rules: response.data!.validation_rules,
|
|
options: response.data!.options,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
)
|
|
}))
|
|
})));
|
|
|
|
// 2025-11-27: itemMasterFields 동기화 (항목탭 실시간 반영)
|
|
// ItemField → ItemMasterField 타입 매핑
|
|
// 2025-11-28: field_key 추가
|
|
setItemMasterFields(prev => prev.map(mf =>
|
|
mf.id === fieldId ? {
|
|
...mf,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value ?? null,
|
|
description: response.data!.placeholder ?? null, // placeholder → description
|
|
validation_rules: response.data!.validation_rules ?? null,
|
|
options: response.data!.options ?? null,
|
|
properties: response.data!.properties ?? null,
|
|
updated_at: response.data!.updated_at,
|
|
} : mf
|
|
));
|
|
|
|
// 2025-11-27: independentFields 동기화 (섹션에 연결되지 않은 필드)
|
|
// 2025-11-28: field_key 추가
|
|
setIndependentFields(prev => prev.map(field =>
|
|
field.id === fieldId ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value,
|
|
placeholder: response.data!.placeholder,
|
|
display_condition: response.data!.display_condition,
|
|
validation_rules: response.data!.validation_rules,
|
|
options: response.data!.options,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
));
|
|
|
|
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
// 2025-11-28: field_key 추가
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).map(field =>
|
|
field.id === fieldId ? {
|
|
...field,
|
|
field_name: response.data!.field_name,
|
|
field_key: response.data!.field_key, // 2025-11-28: field_key 추가
|
|
field_type: response.data!.field_type,
|
|
is_required: response.data!.is_required,
|
|
default_value: response.data!.default_value,
|
|
placeholder: response.data!.placeholder,
|
|
display_condition: response.data!.display_condition,
|
|
validation_rules: response.data!.validation_rules,
|
|
options: response.data!.options,
|
|
properties: response.data!.properties,
|
|
updated_at: response.data!.updated_at,
|
|
} : field
|
|
)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 필드 수정 성공:', fieldId);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteField = async (fieldId: number) => {
|
|
try {
|
|
// API 호출
|
|
const response = await itemMasterApi.fields.delete(fieldId);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '필드 삭제 실패');
|
|
}
|
|
|
|
// state 업데이트
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).filter(field => field.id !== fieldId)
|
|
}))
|
|
})));
|
|
|
|
// 2025-11-27: itemMasterFields 동기화 (항목탭 실시간 반영)
|
|
setItemMasterFields(prev => prev.filter(mf => mf.id !== fieldId));
|
|
|
|
// 2025-11-27: independentFields 동기화
|
|
setIndependentFields(prev => prev.filter(f => f.id !== fieldId));
|
|
|
|
// 2025-11-27: independentSections 동기화 (섹션탭/계층구조 실시간 반영)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
fields: (section.fields || []).filter(field => field.id !== fieldId)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] 필드 삭제 성공:', fieldId);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const reorderFields = async (sectionId: number, fieldIds: number[]) => {
|
|
try {
|
|
// 2025-12-03: 검증 추가 - 빈 배열이면 스킵
|
|
if (!fieldIds || fieldIds.length === 0) {
|
|
console.warn('[ItemMasterContext] reorderFields 스킵: 필드 ID 배열이 비어있음');
|
|
return;
|
|
}
|
|
|
|
// 2025-12-03: 검증 추가 - 유효하지 않은 ID 필터링 (null, undefined, NaN 제거)
|
|
const validFieldIds = fieldIds.filter(id => id !== null && id !== undefined && !isNaN(id));
|
|
if (validFieldIds.length !== fieldIds.length) {
|
|
console.warn('[ItemMasterContext] reorderFields: 유효하지 않은 필드 ID 제거됨', {
|
|
original: fieldIds,
|
|
valid: validFieldIds
|
|
});
|
|
}
|
|
|
|
if (validFieldIds.length === 0) {
|
|
console.warn('[ItemMasterContext] reorderFields 스킵: 유효한 필드 ID가 없음');
|
|
return;
|
|
}
|
|
|
|
// 요청 데이터 구성 (2025-12-03: field_orders → items로 변경, 백엔드와 동일하게)
|
|
const requestData = {
|
|
items: validFieldIds.map((id, index) => ({ id, order_no: index + 1 })) // order_no는 1부터 시작
|
|
};
|
|
|
|
// API 호출
|
|
const response = await itemMasterApi.fields.reorder(sectionId, requestData);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '필드 순서 변경 실패');
|
|
}
|
|
|
|
// 2025-12-03: 백엔드가 'success' 문자열만 반환하므로 로컬 state에서 순서 업데이트
|
|
// validFieldIds 순서대로 필드 재정렬
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => {
|
|
if (section.id !== sectionId) return section;
|
|
|
|
// fieldIds 순서대로 필드 재정렬
|
|
const fieldMap = new Map((section.fields || []).map(f => [f.id, f]));
|
|
const reorderedFields = validFieldIds
|
|
.map((id, index) => {
|
|
const field = fieldMap.get(id);
|
|
return field ? { ...field, order_no: index + 1 } : null;
|
|
})
|
|
.filter((f): f is ItemField => f !== null);
|
|
|
|
return { ...section, fields: reorderedFields };
|
|
})
|
|
})));
|
|
|
|
// 2025-12-03: 섹션탭 실시간 반영 추가
|
|
setIndependentSections(prev => prev.map(section => {
|
|
if (section.id !== sectionId) return section;
|
|
|
|
const fieldMap = new Map((section.fields || []).map(f => [f.id, f]));
|
|
const reorderedFields = validFieldIds
|
|
.map((id, index) => {
|
|
const field = fieldMap.get(id);
|
|
return field ? { ...field, order_no: index + 1 } : null;
|
|
})
|
|
.filter((f): f is ItemField => f !== null);
|
|
|
|
return { ...section, fields: reorderedFields };
|
|
}));
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 순서 변경 실패:', errorMessage);
|
|
// 2025-12-03: ApiError인 경우 상세 정보 출력
|
|
if (error instanceof ApiError && error.errors) {
|
|
console.error('[ItemMasterContext] 상세 검증 에러:', error.errors);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// BOM CRUD with API
|
|
const addBOMItem = async (sectionId: number, bomData: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
|
try {
|
|
// API 호출 (null → undefined 변환)
|
|
const response = await itemMasterApi.bomItems.create(sectionId, {
|
|
item_code: bomData.item_code ?? undefined,
|
|
item_name: bomData.item_name,
|
|
quantity: bomData.quantity,
|
|
unit: bomData.unit ?? undefined,
|
|
unit_price: bomData.unit_price ?? undefined,
|
|
total_price: bomData.total_price ?? undefined,
|
|
spec: bomData.spec ?? undefined,
|
|
note: bomData.note ?? undefined,
|
|
});
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || 'BOM 항목 생성 실패');
|
|
}
|
|
|
|
// 응답 데이터 변환 및 state 업데이트
|
|
const newBOM: BOMItem = {
|
|
id: response.data.id,
|
|
tenant_id: response.data.tenant_id,
|
|
section_id: response.data.section_id,
|
|
item_code: response.data.item_code,
|
|
item_name: response.data.item_name,
|
|
quantity: response.data.quantity,
|
|
unit: response.data.unit,
|
|
unit_price: response.data.unit_price,
|
|
total_price: response.data.total_price,
|
|
spec: response.data.spec,
|
|
note: response.data.note,
|
|
created_at: response.data.created_at,
|
|
updated_at: response.data.updated_at,
|
|
};
|
|
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, bom_items: [...(section.bom_items || []), newBOM] }
|
|
: section
|
|
)
|
|
})));
|
|
|
|
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, bom_items: [...(section.bom_items || []), newBOM] }
|
|
: section
|
|
));
|
|
|
|
console.log('[ItemMasterContext] BOM 항목 생성 성공 (양방향 동기화):', newBOM.id);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] BOM 항목 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const updateBOMItem = async (bomId: number, updates: Partial<BOMItem>) => {
|
|
try {
|
|
// API 호출 (null → undefined 변환)
|
|
const requestData: any = {};
|
|
if (updates.item_code !== undefined) requestData.item_code = updates.item_code ?? undefined;
|
|
if (updates.item_name !== undefined) requestData.item_name = updates.item_name;
|
|
if (updates.quantity !== undefined) requestData.quantity = updates.quantity;
|
|
if (updates.unit !== undefined) requestData.unit = updates.unit ?? undefined;
|
|
if (updates.unit_price !== undefined) requestData.unit_price = updates.unit_price ?? undefined;
|
|
if (updates.total_price !== undefined) requestData.total_price = updates.total_price ?? undefined;
|
|
if (updates.spec !== undefined) requestData.spec = updates.spec ?? undefined;
|
|
if (updates.note !== undefined) requestData.note = updates.note ?? undefined;
|
|
|
|
const response = await itemMasterApi.bomItems.update(bomId, requestData);
|
|
|
|
if (!response.success || !response.data) {
|
|
throw new Error(response.message || 'BOM 항목 수정 실패');
|
|
}
|
|
|
|
// 업데이트된 BOM 데이터
|
|
const updatedBOMData = {
|
|
item_code: response.data!.item_code,
|
|
item_name: response.data!.item_name,
|
|
quantity: response.data!.quantity,
|
|
unit: response.data!.unit,
|
|
unit_price: response.data!.unit_price,
|
|
total_price: response.data!.total_price,
|
|
spec: response.data!.spec,
|
|
note: response.data!.note,
|
|
updated_at: response.data!.updated_at,
|
|
};
|
|
|
|
// state 업데이트
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
bom_items: (section.bom_items || []).map((bom: BOMItem) =>
|
|
bom.id === bomId ? { ...bom, ...updatedBOMData } : bom
|
|
)
|
|
}))
|
|
})));
|
|
|
|
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
bom_items: (section.bom_items || []).map((bom: BOMItem) =>
|
|
bom.id === bomId ? { ...bom, ...updatedBOMData } : bom
|
|
)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] BOM 항목 수정 성공 (양방향 동기화):', bomId);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] BOM 항목 수정 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const deleteBOMItem = async (bomId: number) => {
|
|
try {
|
|
// API 호출
|
|
const response = await itemMasterApi.bomItems.delete(bomId);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || 'BOM 항목 삭제 실패');
|
|
}
|
|
|
|
// state 업데이트
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section => ({
|
|
...section,
|
|
bom_items: (section.bom_items || []).filter((bom: BOMItem) => bom.id !== bomId)
|
|
}))
|
|
})));
|
|
|
|
// 2025-11-27: independentSections 동기화 (섹션탭 실시간 반영)
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section => ({
|
|
...section,
|
|
bom_items: (section.bom_items || []).filter((bom: BOMItem) => bom.id !== bomId)
|
|
})));
|
|
|
|
console.log('[ItemMasterContext] BOM 항목 삭제 성공 (양방향 동기화):', bomId);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] BOM 항목 삭제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ============================================
|
|
// 2025-11-26 추가: 독립 엔티티 관리
|
|
// ============================================
|
|
|
|
/**
|
|
* 독립 섹션 로드 (API 호출 없이 state 설정)
|
|
*/
|
|
const loadIndependentSections = (sections: ItemSection[]) => {
|
|
setIndependentSections(sections);
|
|
console.log('[ItemMasterContext] 독립 섹션 로드 완료:', sections.length);
|
|
};
|
|
|
|
/**
|
|
* 독립 필드 로드 (API 호출 없이 state 설정)
|
|
*/
|
|
const loadIndependentFields = (fields: ItemField[]) => {
|
|
setIndependentFields(fields);
|
|
console.log('[ItemMasterContext] 독립 필드 로드 완료:', fields.length);
|
|
};
|
|
|
|
/**
|
|
* 독립 BOM 로드 (API 호출 없이 state 설정)
|
|
*/
|
|
const loadIndependentBomItems = (bomItems: BOMItem[]) => {
|
|
setIndependentBomItems(bomItems);
|
|
console.log('[ItemMasterContext] 독립 BOM 로드 완료:', bomItems.length);
|
|
};
|
|
|
|
/**
|
|
* 독립 섹션 새로고침 (API 호출)
|
|
* @param isTemplate - true면 템플릿만, false면 일반 독립 섹션만, undefined면 전체
|
|
*/
|
|
const refreshIndependentSections = async (isTemplate?: boolean) => {
|
|
try {
|
|
// 2025-11-26: sections.list()는 ItemSectionResponse[]를 직접 반환
|
|
const sectionsData = await itemMasterApi.sections.list({ is_template: isTemplate });
|
|
|
|
// API가 배열을 직접 반환하므로 바로 변환
|
|
const sections = sectionsData.map(transformSectionResponse);
|
|
setIndependentSections(sections);
|
|
console.log('[ItemMasterContext] 독립 섹션 새로고침 완료:', sections.length);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 섹션 새로고침 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 독립 필드 새로고침 (API 호출)
|
|
*/
|
|
const refreshIndependentFields = async () => {
|
|
try {
|
|
// 2025-11-26: fields.list()는 ItemFieldResponse[]를 직접 반환
|
|
const fieldsData = await itemMasterApi.fields.list();
|
|
|
|
// API가 배열을 직접 반환하므로 바로 변환
|
|
const fields = fieldsData.map(transformFieldResponse);
|
|
setIndependentFields(fields);
|
|
console.log('[ItemMasterContext] 독립 필드 새로고침 완료:', fields.length);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 필드 새로고침 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 독립 BOM 새로고침 (API 호출)
|
|
*/
|
|
const refreshIndependentBomItems = async () => {
|
|
try {
|
|
// 2025-11-26: bomItems.list()는 BomItemResponse[]를 직접 반환
|
|
const bomItemsData = await itemMasterApi.bomItems.list();
|
|
|
|
// API가 배열을 직접 반환하므로 바로 변환
|
|
const bomItems = bomItemsData.map(transformBomItemResponse);
|
|
setIndependentBomItems(bomItems);
|
|
console.log('[ItemMasterContext] 독립 BOM 새로고침 완료:', bomItems.length);
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 BOM 새로고침 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 독립 섹션 생성 (API 호출)
|
|
* 2025-11-26: sections.createIndependent()는 ItemSectionResponse를 직접 반환
|
|
*/
|
|
const createIndependentSection = async (data: Omit<IndependentSectionRequest, 'group_id'>): Promise<ItemSection> => {
|
|
try {
|
|
// API가 ItemSectionResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const sectionData = await itemMasterApi.sections.createIndependent(data);
|
|
|
|
const newSection = transformSectionResponse(sectionData);
|
|
setIndependentSections(prev => [...prev, newSection]);
|
|
console.log('[ItemMasterContext] 독립 섹션 생성 성공:', newSection.id);
|
|
return newSection;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 섹션 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 독립 필드 생성 (API 호출)
|
|
* 2025-11-26: fields.createIndependent()는 ItemFieldResponse를 직접 반환
|
|
*/
|
|
const createIndependentField = async (data: Omit<IndependentFieldRequest, 'group_id'>): Promise<ItemField> => {
|
|
try {
|
|
// API가 ItemFieldResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const fieldData = await itemMasterApi.fields.createIndependent(data);
|
|
|
|
const newField = transformFieldResponse(fieldData);
|
|
setIndependentFields(prev => [...prev, newField]);
|
|
console.log('[ItemMasterContext] 독립 필드 생성 성공:', newField.id);
|
|
return newField;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 필드 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 독립 BOM 생성 (API 호출)
|
|
* 2025-11-26: bomItems.createIndependent()는 BomItemResponse를 직접 반환
|
|
*/
|
|
const createIndependentBomItem = async (data: Omit<IndependentBomItemRequest, 'group_id'>): Promise<BOMItem> => {
|
|
try {
|
|
// API가 BomItemResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const bomItemData = await itemMasterApi.bomItems.createIndependent(data);
|
|
|
|
const newBomItem = transformBomItemResponse(bomItemData);
|
|
setIndependentBomItems(prev => [...prev, newBomItem]);
|
|
console.log('[ItemMasterContext] 독립 BOM 생성 성공:', newBomItem.id);
|
|
return newBomItem;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 독립 BOM 생성 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ============================================
|
|
// 2025-11-26 추가: 링크/언링크 관리
|
|
// ============================================
|
|
|
|
/**
|
|
* 섹션을 페이지에 연결
|
|
*/
|
|
const linkSectionToPage = async (pageId: number, sectionId: number, orderNo?: number) => {
|
|
try {
|
|
const response = await itemMasterApi.pages.linkSection(pageId, {
|
|
child_id: sectionId,
|
|
order_no: orderNo,
|
|
});
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '섹션 연결 실패');
|
|
}
|
|
|
|
// 페이지 구조 새로고침
|
|
// 2025-11-26: API가 PageStructureResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const structureData = await itemMasterApi.pages.getStructure(pageId);
|
|
console.log('[linkSectionToPage] getStructure 응답:', structureData);
|
|
console.log('[linkSectionToPage] sections 배열:', structureData?.sections);
|
|
if (structureData?.sections?.[0]) {
|
|
console.log('[linkSectionToPage] 첫번째 섹션 원본:', structureData.sections[0]);
|
|
console.log('[linkSectionToPage] 첫번째 섹션 키:', Object.keys(structureData.sections[0]));
|
|
}
|
|
|
|
if (structureData && structureData.page) {
|
|
const updatedPage = transformPageResponse(structureData.page);
|
|
|
|
// getStructure API 응답 형식: { section: {...}, order_no, fields: [], bom_items: [] }
|
|
// section wrapper를 해제하고 fields/bom_items를 병합
|
|
updatedPage.sections = structureData.sections?.map((sectionData: any) => {
|
|
// sectionData가 wrapper 형태인지 확인
|
|
const rawSection = sectionData.section || sectionData;
|
|
const transformed = transformSectionResponse(rawSection);
|
|
|
|
// fields와 bom_items는 wrapper에서 직접 가져옴 (있다면)
|
|
if (sectionData.fields && Array.isArray(sectionData.fields)) {
|
|
transformed.fields = sectionData.fields.map((f: any) =>
|
|
f.field ? transformFieldResponse(f.field) : transformFieldResponse(f)
|
|
);
|
|
}
|
|
if (sectionData.bom_items && Array.isArray(sectionData.bom_items)) {
|
|
transformed.bom_items = sectionData.bom_items.map((b: any) =>
|
|
b.bom_item ? transformBomItemResponse(b.bom_item) : transformBomItemResponse(b)
|
|
);
|
|
}
|
|
|
|
return transformed;
|
|
}) || [];
|
|
|
|
console.log('[linkSectionToPage] 변환된 페이지:', updatedPage);
|
|
console.log('[linkSectionToPage] 변환된 sections:', updatedPage.sections);
|
|
setItemPages(prev => prev.map(page => page.id === pageId ? updatedPage : page));
|
|
} else {
|
|
console.warn('[linkSectionToPage] structureData.page가 없음, 페이지 데이터를 다시 로드해주세요');
|
|
// TODO: 전체 init 데이터 새로고침 기능 구현 필요
|
|
// 현재는 페이지를 새로고침하거나 init API를 다시 호출해야 함
|
|
}
|
|
|
|
// 독립 섹션 목록에서 제거 (연결되면 더 이상 독립이 아님)
|
|
setIndependentSections(prev => prev.filter(s => s.id !== sectionId));
|
|
|
|
console.log('[ItemMasterContext] 섹션 연결 성공:', { pageId, sectionId });
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 연결 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 섹션을 페이지에서 연결 해제
|
|
*/
|
|
const unlinkSectionFromPage = async (pageId: number, sectionId: number) => {
|
|
try {
|
|
const response = await itemMasterApi.pages.unlinkSection(pageId, sectionId);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '섹션 연결 해제 실패');
|
|
}
|
|
|
|
// 페이지에서 섹션 제거
|
|
setItemPages(prev => prev.map(page =>
|
|
page.id === pageId
|
|
? { ...page, sections: page.sections.filter(s => s.id !== sectionId) }
|
|
: page
|
|
));
|
|
|
|
// 독립 섹션 목록 새로고침 (연결 해제된 섹션이 추가됨)
|
|
// 2025-11-26: refresh 실패는 치명적이지 않으므로 에러 로그만 남기고 진행
|
|
try {
|
|
await refreshIndependentSections();
|
|
} catch (refreshError) {
|
|
console.warn('[ItemMasterContext] 독립 섹션 새로고침 실패 (무시됨):', getErrorMessage(refreshError));
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 섹션 연결 해제 성공:', { pageId, sectionId });
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 연결 해제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 필드를 섹션에 연결
|
|
* 2025-12-01: fieldData 파라미터 추가 (createIndependentField 직후 호출 시 상태 동기화 이슈 해결)
|
|
*/
|
|
const linkFieldToSection = async (sectionId: number, fieldId: number, orderNo?: number, fieldData?: ItemField) => {
|
|
try {
|
|
const response = await itemMasterApi.sections.linkField(sectionId, {
|
|
child_id: fieldId,
|
|
order_no: orderNo,
|
|
});
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '필드 연결 실패');
|
|
}
|
|
|
|
// 2025-12-01: fieldData가 직접 전달되면 사용, 아니면 independentFields에서 찾기
|
|
// (createIndependentField 직후 호출 시 상태가 아직 업데이트되지 않아 find가 실패할 수 있음)
|
|
const linkedField = fieldData || independentFields.find(f => f.id === fieldId);
|
|
if (linkedField) {
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === sectionId
|
|
? {
|
|
...section,
|
|
fields: [
|
|
...(section.fields || []),
|
|
{
|
|
...linkedField,
|
|
section_id: sectionId,
|
|
// 2025-12-02: 섹션별 순서 종속 - 새 order_no 할당
|
|
order_no: orderNo ?? section.fields?.length ?? 0
|
|
}
|
|
]
|
|
}
|
|
: section
|
|
)
|
|
})));
|
|
|
|
// 2025-11-27: 섹션탭 실시간 반영
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section =>
|
|
section.id === sectionId
|
|
? {
|
|
...section,
|
|
fields: [
|
|
...(section.fields || []),
|
|
{
|
|
...linkedField,
|
|
section_id: sectionId,
|
|
// 2025-12-02: 섹션별 순서 종속 - 새 order_no 할당
|
|
order_no: orderNo ?? section.fields?.length ?? 0
|
|
}
|
|
]
|
|
}
|
|
: section
|
|
));
|
|
}
|
|
|
|
// 독립 필드 목록에서 제거 (fieldData가 전달된 경우에도 실행)
|
|
setIndependentFields(prev => prev.filter(f => f.id !== fieldId));
|
|
|
|
console.log('[ItemMasterContext] 필드 연결 성공:', { sectionId, fieldId });
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 연결 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 필드를 섹션에서 연결 해제
|
|
*/
|
|
const unlinkFieldFromSection = async (sectionId: number, fieldId: number) => {
|
|
try {
|
|
const response = await itemMasterApi.sections.unlinkField(sectionId, fieldId);
|
|
|
|
if (!response.success) {
|
|
throw new Error(response.message || '필드 연결 해제 실패');
|
|
}
|
|
|
|
// 섹션에서 필드 제거
|
|
setItemPages(prev => prev.map(page => ({
|
|
...page,
|
|
sections: page.sections.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, fields: (section.fields || []).filter(f => f.id !== fieldId) }
|
|
: section
|
|
)
|
|
})));
|
|
|
|
// 2025-11-27: 섹션탭 실시간 반영
|
|
// sectionsAsTemplates useMemo가 [itemPages, independentSections] 둘 다 의존
|
|
setIndependentSections(prev => prev.map(section =>
|
|
section.id === sectionId
|
|
? { ...section, fields: (section.fields || []).filter(f => f.id !== fieldId) }
|
|
: section
|
|
));
|
|
|
|
// 독립 필드 목록 새로고침
|
|
await refreshIndependentFields();
|
|
|
|
console.log('[ItemMasterContext] 필드 연결 해제 성공:', { sectionId, fieldId });
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 연결 해제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ============================================
|
|
// 2025-11-26 추가: 사용처 조회
|
|
// ============================================
|
|
|
|
/**
|
|
* 섹션 사용처 조회
|
|
*/
|
|
const getSectionUsage = async (sectionId: number): Promise<SectionUsageResponse> => {
|
|
try {
|
|
// 2025-11-26: API가 SectionUsageResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const usageData = await itemMasterApi.sections.getUsage(sectionId);
|
|
|
|
console.log('[ItemMasterContext] 섹션 사용처 조회 성공:', sectionId);
|
|
return usageData;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 사용처 조회 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 필드 사용처 조회
|
|
*/
|
|
const getFieldUsage = async (fieldId: number): Promise<FieldUsageResponse> => {
|
|
try {
|
|
// 2025-11-26: API가 FieldUsageResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const usageData = await itemMasterApi.fields.getUsage(fieldId);
|
|
|
|
console.log('[ItemMasterContext] 필드 사용처 조회 성공:', fieldId);
|
|
return usageData;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 사용처 조회 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ============================================
|
|
// 2025-11-26 추가: 복제 기능
|
|
// ============================================
|
|
|
|
/**
|
|
* 섹션 복제
|
|
*/
|
|
const cloneSection = async (sectionId: number): Promise<ItemSection> => {
|
|
try {
|
|
// 2025-11-26: API가 ItemSectionResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const sectionData = await itemMasterApi.sections.clone(sectionId);
|
|
console.log('[cloneSection] API 응답 원본:', sectionData);
|
|
|
|
const clonedSection = transformSectionResponse(sectionData);
|
|
console.log('[cloneSection] 변환 후 섹션:', {
|
|
id: clonedSection.id,
|
|
title: clonedSection.title,
|
|
page_id: clonedSection.page_id,
|
|
section_type: clonedSection.section_type,
|
|
fieldsCount: clonedSection.fields?.length || 0,
|
|
fields: clonedSection.fields,
|
|
});
|
|
|
|
// 2025-11-27: 복제된 섹션을 적절한 상태에 추가 (즉시 UI 업데이트)
|
|
// page_id가 null 또는 undefined면 독립 섹션 (API가 null 대신 undefined를 반환할 수 있음)
|
|
if (clonedSection.page_id == null) {
|
|
// 독립 섹션(일반 섹션)인 경우 independentSections에 추가
|
|
console.log('[cloneSection] 독립 섹션 추가 (independentSections)');
|
|
setIndependentSections(prev => {
|
|
const newSections = [...prev, clonedSection];
|
|
console.log('[cloneSection] independentSections 업데이트:', newSections.length);
|
|
return newSections;
|
|
});
|
|
} else {
|
|
// 모듈 섹션인 경우 해당 페이지의 sections에 추가
|
|
console.log('[cloneSection] 모듈 섹션 추가 (itemPages), page_id:', clonedSection.page_id);
|
|
setItemPages(prev => {
|
|
const newPages = prev.map(page => {
|
|
if (page.id === clonedSection.page_id) {
|
|
console.log('[cloneSection] 페이지 찾음:', page.id, '기존 섹션 수:', page.sections.length);
|
|
return {
|
|
...page,
|
|
sections: [...page.sections, clonedSection]
|
|
};
|
|
}
|
|
return page;
|
|
});
|
|
return newPages;
|
|
});
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 섹션 복제 성공:', clonedSection.id);
|
|
return clonedSection;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 섹션 복제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 필드 복제
|
|
*/
|
|
const cloneField = async (fieldId: number): Promise<ItemField> => {
|
|
try {
|
|
// 2025-11-26: API가 ItemFieldResponse를 직접 반환 (ApiResponse 래퍼 없음)
|
|
const fieldData = await itemMasterApi.fields.clone(fieldId);
|
|
|
|
const clonedField = transformFieldResponse(fieldData);
|
|
|
|
// 복제된 필드가 독립 필드면 독립 필드 목록에 추가
|
|
if (clonedField.section_id === null) {
|
|
setIndependentFields(prev => [...prev, clonedField]);
|
|
}
|
|
|
|
console.log('[ItemMasterContext] 필드 복제 성공:', clonedField.id);
|
|
return clonedField;
|
|
} catch (error) {
|
|
const errorMessage = getErrorMessage(error);
|
|
console.error('[ItemMasterContext] 필드 복제 실패:', errorMessage);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// 캐시 및 데이터 초기화 함수
|
|
const clearCache = () => {
|
|
if (cache) {
|
|
cache.clear();
|
|
console.log('[ItemMasterContext] TenantAwareCache cleared');
|
|
}
|
|
};
|
|
|
|
const resetAllData = () => {
|
|
// 모든 state를 초기값으로 reset
|
|
setItemMasters(initialItemMasters);
|
|
setSpecificationMasters(initialSpecificationMasters);
|
|
setMaterialItemNames(initialMaterialItemNames);
|
|
setItemCategories(initialItemCategories);
|
|
setItemUnits(initialItemUnits);
|
|
setItemMaterials(initialItemMaterials);
|
|
setSurfaceTreatments(initialSurfaceTreatments);
|
|
setPartTypeOptions(initialPartTypeOptions);
|
|
setPartUsageOptions(initialPartUsageOptions);
|
|
setGuideRailOptions(initialGuideRailOptions);
|
|
setSectionTemplates([]);
|
|
setItemMasterFields(initialItemMasterFields);
|
|
setItemPages(initialItemPages);
|
|
|
|
// TenantAwareCache도 정리
|
|
clearCache();
|
|
|
|
console.log('[ItemMasterContext] All data reset to initial state');
|
|
};
|
|
|
|
// Context value
|
|
const value: ItemMasterContextType = {
|
|
itemMasters,
|
|
addItemMaster,
|
|
updateItemMaster,
|
|
deleteItemMaster,
|
|
|
|
specificationMasters,
|
|
addSpecificationMaster,
|
|
updateSpecificationMaster,
|
|
deleteSpecificationMaster,
|
|
|
|
materialItemNames,
|
|
addMaterialItemName,
|
|
updateMaterialItemName,
|
|
deleteMaterialItemName,
|
|
|
|
itemMasterFields,
|
|
loadItemMasterFields,
|
|
addItemMasterField,
|
|
updateItemMasterField,
|
|
deleteItemMasterField,
|
|
|
|
sectionTemplates,
|
|
loadSectionTemplates,
|
|
addSectionTemplate,
|
|
updateSectionTemplate,
|
|
deleteSectionTemplate,
|
|
|
|
itemPages,
|
|
loadItemPages,
|
|
addItemPage,
|
|
updateItemPage,
|
|
deleteItemPage,
|
|
reorderPages,
|
|
addSectionToPage,
|
|
updateSection,
|
|
deleteSection,
|
|
reorderSections,
|
|
addFieldToSection,
|
|
updateField,
|
|
deleteField,
|
|
reorderFields,
|
|
|
|
addBOMItem,
|
|
updateBOMItem,
|
|
deleteBOMItem,
|
|
|
|
// 2025-11-26 추가: 독립 엔티티 관리
|
|
independentSections,
|
|
independentFields,
|
|
independentBomItems,
|
|
loadIndependentSections,
|
|
loadIndependentFields,
|
|
loadIndependentBomItems,
|
|
refreshIndependentSections,
|
|
refreshIndependentFields,
|
|
refreshIndependentBomItems,
|
|
createIndependentSection,
|
|
createIndependentField,
|
|
createIndependentBomItem,
|
|
|
|
// 링크/언링크 관리
|
|
linkSectionToPage,
|
|
unlinkSectionFromPage,
|
|
linkFieldToSection,
|
|
unlinkFieldFromSection,
|
|
|
|
// 사용처 조회
|
|
getSectionUsage,
|
|
getFieldUsage,
|
|
|
|
// 복제
|
|
cloneSection,
|
|
cloneField,
|
|
|
|
itemCategories,
|
|
itemUnits,
|
|
itemMaterials,
|
|
surfaceTreatments,
|
|
partTypeOptions,
|
|
partUsageOptions,
|
|
guideRailOptions,
|
|
|
|
addItemCategory,
|
|
updateItemCategory,
|
|
deleteItemCategory,
|
|
|
|
addItemUnit,
|
|
updateItemUnit,
|
|
deleteItemUnit,
|
|
|
|
addItemMaterial,
|
|
updateItemMaterial,
|
|
deleteItemMaterial,
|
|
|
|
addSurfaceTreatment,
|
|
updateSurfaceTreatment,
|
|
deleteSurfaceTreatment,
|
|
|
|
addPartTypeOption,
|
|
updatePartTypeOption,
|
|
deletePartTypeOption,
|
|
|
|
addPartUsageOption,
|
|
updatePartUsageOption,
|
|
deletePartUsageOption,
|
|
|
|
addGuideRailOption,
|
|
updateGuideRailOption,
|
|
deleteGuideRailOption,
|
|
|
|
clearCache,
|
|
resetAllData,
|
|
|
|
tenantId,
|
|
};
|
|
|
|
return (
|
|
<ItemMasterContext.Provider value={value}>
|
|
{children}
|
|
</ItemMasterContext.Provider>
|
|
);
|
|
}
|
|
|
|
// Custom hook
|
|
export function useItemMaster() {
|
|
const context = useContext(ItemMasterContext);
|
|
if (context === undefined) {
|
|
throw new Error('useItemMaster must be used within an ItemMasterProvider');
|
|
}
|
|
return context;
|
|
}
|