Files
sam-react-prod/src/types/item-master-api.ts
byeongcheolryu 52b8b1f0be feat: 품목기준관리 Zustand 리팩토링 (테스트 페이지)
## 주요 변경사항
- Zustand 정규화 스토어 구현 (useItemMasterStore)
- 테스트 페이지 구현 (/items-management-test)
- 계층구조/섹션/항목/속성 탭 완성
- CRUD 다이얼로그 (페이지/섹션/필드/BOM/속성)
- Import 기능 (섹션/필드 불러오기)
- 드래그앤드롭 순서 변경
- 인라인 편집 기능

## 구현 완료 (약 72%)
- 페이지/섹션/필드 CRUD 
- BOM 관리 
- 단위/재질/표면처리 CRUD 
- Import/복제 기능 

## 미구현 기능
- 절대경로(absolute_path) 수정
- 페이지 복제
- 필드 조건부 표시
- 칼럼 관리

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 09:04:28 +09:00

734 lines
19 KiB
TypeScript

// 품목기준관리 API 타입 정의
// API 응답 기준 snake_case 사용
// ============================================
// 공통 타입
// ============================================
/**
* 표준 API 응답 래퍼
*/
export interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
/**
* 페이지네이션 메타데이터
*/
export interface PaginationMeta {
current_page: number;
per_page: number;
total: number;
last_page: number;
}
// ============================================
// 초기화 API
// ============================================
/**
* 초기화 API 응답 - 화면 진입 시 전체 데이터 로드
* GET /v1/item-master/init
*
* 2025-11-27 변경사항:
* - masterFields → 독립 필드(item_fields WHERE section_id IS NULL)로 대체 예정
* - fields 필드 추가: 모든 필드 목록 (독립 필드 + 섹션 내 필드)
*/
export interface InitResponse {
pages: ItemPageResponse[];
sections?: ItemSectionResponse[]; // 2025-11-26 추가: 모든 섹션 (독립 섹션 포함)
fields?: ItemFieldResponse[]; // 2025-11-27 추가: 모든 필드 (독립 필드 = section_id IS NULL)
sectionTemplates: SectionTemplateResponse[];
/** @deprecated 2025-11-27: item_fields로 통합됨. fields 필드 사용 권장 */
masterFields: MasterFieldResponse[];
customTabs: CustomTabResponse[];
tabColumns: Record<number, TabColumnResponse[]>; // tab_id를 key로 사용
unitOptions: UnitOptionResponse[];
}
// ============================================
// 페이지 관리
// ============================================
/**
* 페이지 생성/수정 요청
* POST /v1/item-master/pages
* PUT /v1/item-master/pages/{id}
*/
export interface ItemPageRequest {
page_name: string;
item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
absolute_path?: string;
is_active?: boolean;
}
/**
* 페이지 응답
*/
export interface ItemPageResponse {
id: number;
tenant_id: number;
page_name: string;
item_type: string;
description: string | null; // 2025-11-26 추가: 페이지 설명
absolute_path: string | null;
is_active: boolean;
order_no: number; // 2025-11-26 추가: 순서 번호
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
sections?: ItemSectionResponse[]; // Nested 조회 시 포함
}
/**
* 페이지 순서 변경 요청
* PUT /v1/item-master/pages/reorder (향후 구현 가능성)
*/
export interface PageReorderRequest {
page_orders: Array<{
id: number;
order_no: number;
}>;
}
// ============================================
// 섹션 관리
// ============================================
/**
* 섹션 생성/수정 요청
* POST /v1/item-master/pages/{pageId}/sections
* PUT /v1/item-master/sections/{id}
*/
export interface ItemSectionRequest {
title: string;
type: 'fields' | 'bom';
}
/**
* 섹션 응답
* 2025-11-26: section_templates 테이블 통합으로 is_template, is_default, description, group_id 추가
*/
export interface ItemSectionResponse {
id: number;
tenant_id: number;
group_id: number | null; // 그룹 ID (독립 섹션 그룹화용)
page_id: number | null; // 페이지 ID (null이면 독립 섹션)
title: string;
type: 'fields' | 'bom';
order_no: number;
is_template: boolean; // 템플릿 여부 (section_templates 통합)
is_default: boolean; // 기본 템플릿 여부
description: string | null; // 섹션 설명
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
fields?: ItemFieldResponse[]; // Nested 조회 시 포함
bomItems?: BomItemResponse[]; // Nested 조회 시 포함 (camelCase)
bom_items?: BomItemResponse[]; // 2025-11-27: 백엔드가 snake_case로 반환
}
/**
* 섹션 순서 변경 요청
* PUT /v1/item-master/pages/{pageId}/sections/reorder
* 백엔드 API는 'items' 필드를 기대함
*/
export interface SectionReorderRequest {
items: Array<{
id: number;
order_no: number;
}>;
}
/**
* 독립 섹션 생성 요청
* POST /v1/item-master/sections
* 2025-11-26 신규 API
*/
export interface IndependentSectionRequest {
group_id?: number;
title: string;
type: 'fields' | 'bom';
is_template?: boolean;
is_default?: boolean;
description?: string;
}
/**
* 섹션 사용처 응답
* GET /v1/item-master/sections/{id}/usage
* 2025-11-26 신규 API
*/
export interface SectionUsageResponse {
section_id: number;
direct_page: {
id: number;
page_name: string;
item_type: string;
} | null;
linked_pages: Array<{
id: number;
page_name: string;
item_type: string;
}>;
total_usage_count: number;
}
/**
* 페이지에 섹션 연결 요청
* POST /v1/item-master/pages/{id}/link-section
* 2025-11-26 신규 API
*/
export interface LinkSectionRequest {
child_id: number; // 연결할 섹션 ID (백엔드: child_id)
order_no?: number;
}
// ============================================
// 필드 관리
// ============================================
/**
* 필드 생성/수정 요청
* POST /v1/item-master/sections/{sectionId}/fields
* PUT /v1/item-master/fields/{id}
*/
export interface ItemFieldRequest {
field_name: string;
field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작)
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
is_required?: boolean;
placeholder?: string;
default_value?: string;
display_condition?: Record<string, any>; // {"field_id": "1", "operator": "equals", "value": "true"}
validation_rules?: Record<string, any>; // {"min": 0, "max": 100, "pattern": "regex"}
options?: Array<{ label: string; value: string }>; // dropdown 옵션
properties?: Record<string, any>; // {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}
is_locked?: boolean; // 2025-11-28: 잠금 여부
}
/**
* 필드 응답
* 2025-11-26: section_id를 nullable로 변경 (독립 필드 지원)
* 2025-11-27: item_master_fields 테이블 통합으로 category, description, is_common 추가
*/
export interface ItemFieldResponse {
id: number;
tenant_id: number;
group_id: number | null; // 그룹 ID (독립 필드 그룹화용)
section_id: number | null; // 섹션 ID (null이면 독립 필드/마스터 항목)
master_field_id?: number | null; // 마스터 항목 ID (마스터에서 가져온 경우)
field_name: string;
field_key: string | null; // 2025-11-28: 필드 키 (형식: {ID}_{사용자입력})
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
order_no: number;
is_required: boolean;
placeholder: string | null;
default_value: string | null;
display_condition: Record<string, any> | null;
validation_rules: Record<string, any> | null;
options: Array<{ label: string; value: string }> | null;
properties: Record<string, any> | null;
// 2025-11-27 추가: item_master_fields 통합으로 추가된 필드
category: string | null; // 카테고리 (예: "공통", "완제품", "부품")
description: string | null; // 필드 설명
is_common: boolean; // 공통 필드 여부
// 2025-11-28 추가: 잠금 기능
is_locked: boolean; // 잠금 여부
locked_by: number | null; // 잠금 설정자
locked_at: string | null; // 잠금 시간
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
/**
* 필드 순서 변경 요청
* PUT /v1/item-master/sections/{sectionId}/fields/reorder
* 백엔드 API는 'items' 필드를 기대함 (SectionReorderRequest와 동일)
*/
export interface FieldReorderRequest {
items: Array<{
id: number;
order_no: number;
}>;
}
/**
* 독립 필드 생성 요청
* POST /v1/item-master/fields
* 2025-11-26 신규 API
* 2025-11-27: item_master_fields 통합으로 category, description, is_common 추가
*/
export interface IndependentFieldRequest {
group_id?: number;
field_name: string;
field_key?: string; // 2025-11-28: 필드 키 (영문, 숫자, 언더스코어만 허용, 영문으로 시작)
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
is_required?: boolean;
default_value?: string;
placeholder?: string;
options?: Array<{ label: string; value: string }>;
properties?: Record<string, any>;
validation_rules?: Record<string, any>;
display_condition?: Record<string, any>;
// 2025-11-27 추가: item_master_fields 통합으로 추가된 필드
category?: string;
description?: string;
is_common?: boolean;
is_locked?: boolean; // 2025-11-28: 잠금 여부
}
/**
* 필드 사용처 응답
* GET /v1/item-master/fields/{id}/usage
* 2025-11-26 신규 API
*/
export interface FieldUsageResponse {
field_id: number;
direct_section: {
id: number;
title: string;
type: 'fields' | 'bom';
} | null;
linked_sections: Array<{
id: number;
title: string;
type: 'fields' | 'bom';
}>;
total_usage_count: number;
}
/**
* 섹션에 필드 연결 요청
* POST /v1/item-master/sections/{id}/link-field
* 2025-11-26 신규 API
*/
export interface LinkFieldRequest {
child_id: number; // 연결할 필드 ID (백엔드: child_id)
order_no?: number;
}
// ============================================
// BOM 관리
// ============================================
/**
* BOM 항목 생성/수정 요청
* POST /v1/item-master/sections/{sectionId}/bom-items
* PUT /v1/item-master/bom-items/{id}
*/
export interface BomItemRequest {
item_code?: string;
item_name: string;
quantity: number;
unit?: string;
unit_price?: number;
total_price?: number;
spec?: string;
note?: string;
}
/**
* BOM 항목 응답
* 2025-11-26: section_id를 nullable로 변경 (독립 BOM 지원)
*/
export interface BomItemResponse {
id: number;
tenant_id: number;
group_id: number | null; // 그룹 ID (독립 BOM 그룹화용)
section_id: number | null; // 섹션 ID (null이면 독립 BOM)
item_code: string | null;
item_name: string;
quantity: number;
unit: string | null;
unit_price: number | null;
total_price: number | null;
spec: string | null;
note: string | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
/**
* 독립 BOM 항목 생성 요청
* POST /v1/item-master/bom-items
* 2025-11-26 신규 API
*/
export interface IndependentBomItemRequest {
group_id?: number;
item_code?: string;
item_name: string;
quantity?: number;
unit?: string;
unit_price?: number;
spec?: string;
note?: string;
}
// ============================================
// 엔티티 관계 (Entity Relationships)
// 2025-11-27 신규: 독립 엔티티 + 링크 테이블 구조
// ============================================
/**
* 엔티티 타입 상수
*/
export type EntityType = 'page' | 'section' | 'field' | 'bom';
export type ParentEntityType = 'page' | 'section';
export type ChildEntityType = 'section' | 'field' | 'bom';
/**
* 엔티티 관계 응답
* entity_relationships 테이블의 레코드
*/
export interface EntityRelationshipResponse {
id: number;
tenant_id: number;
group_id: number; // 그룹 ID (1: 품목관리)
parent_type: ParentEntityType;
parent_id: number;
child_type: ChildEntityType;
child_id: number;
order_no: number;
metadata: Record<string, any> | null;
created_at: string;
updated_at: string;
}
/**
* 엔티티 연결 요청 (공통)
* POST /pages/{pageId}/link-section
* POST /pages/{pageId}/link-field
* POST /sections/{sectionId}/link-field
* POST /sections/{sectionId}/link-bom
*/
export interface LinkEntityRequest {
child_id: number;
order_no?: number;
}
/**
* 관계 순서 변경 요청
* POST /v1/item-master/relationships/reorder
*/
export interface ReorderRelationshipsRequest {
parent_type: ParentEntityType;
parent_id: number;
ordered_items: Array<{
child_type: ChildEntityType;
child_id: number;
}>;
}
/**
* 섹션에 BOM 연결 요청
* POST /v1/item-master/sections/{id}/link-bom
*/
export interface LinkBomRequest {
child_id: number;
order_no?: number;
}
// ============================================
// 페이지 구조 조회
// ============================================
/**
* 페이지 전체 구조 응답
* GET /v1/item-master/pages/{id}/structure
* 2025-11-27 업데이트: 링크 테이블 기반 구조
*/
export interface PageStructureResponse {
page: ItemPageResponse;
sections: Array<{
section: ItemSectionResponse;
order_no: number;
fields: Array<{
field: ItemFieldResponse;
order_no: number;
}>;
bom_items: Array<{
bom_item: BomItemResponse;
order_no: number;
}>;
}>;
direct_fields: Array<{
field: ItemFieldResponse;
order_no: number;
}>;
}
// ============================================
// 섹션 템플릿
// ============================================
/**
* 섹션 템플릿 생성/수정 요청
* POST /v1/item-master/section-templates
* PUT /v1/item-master/section-templates/{id}
*/
export interface SectionTemplateRequest {
title: string;
type: 'fields' | 'bom';
description?: string;
is_default?: boolean;
}
/**
* 섹션 템플릿 응답
*/
export interface SectionTemplateResponse {
id: number;
tenant_id: number;
title: string;
type: 'fields' | 'bom';
description: string | null;
is_default: boolean;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 마스터 필드 (DEPRECATED)
// 2025-11-27: item_master_fields 테이블이 item_fields로 통합됨
// 독립 필드 = item_fields WHERE section_id IS NULL
// IndependentFieldRequest 및 ItemFieldResponse 사용 권장
// ============================================
/**
* 마스터 필드 생성/수정 요청
* @deprecated 2025-11-27: item_fields로 통합됨. IndependentFieldRequest 사용 권장
* POST /v1/item-master/master-fields → POST /v1/item-master/fields 사용
* PUT /v1/item-master/master-fields/{id} → PUT /v1/item-master/fields/{id} 사용
*/
export interface MasterFieldRequest {
field_name: string;
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
category?: string;
description?: string;
is_common?: boolean;
default_value?: string;
options?: Array<{ label: string; value: string }>;
validation_rules?: Record<string, any>;
properties?: Record<string, any>;
}
/**
* 마스터 필드 응답
* @deprecated 2025-11-27: item_fields로 통합됨. ItemFieldResponse 사용 권장
* 독립 필드 조회: GET /v1/item-master/fields?section_id=null
*/
export interface MasterFieldResponse {
id: number;
tenant_id: number;
field_name: string;
field_key: string | null; // 2025-11-28: 필드 키 추가 (형식: {ID}_{사용자입력})
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
category: string | null;
description: string | null;
is_common: boolean;
default_value: string | null;
options: Array<{ label: string; value: string }> | null;
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;
}
// ============================================
// 커스텀 탭
// ============================================
/**
* 커스텀 탭 생성/수정 요청
* POST /v1/item-master/custom-tabs
* PUT /v1/item-master/custom-tabs/{id}
*/
export interface CustomTabRequest {
label: string;
icon?: string;
is_default?: boolean;
}
/**
* 커스텀 탭 응답
*/
export interface CustomTabResponse {
id: number;
tenant_id: number;
label: string;
icon: string | null;
is_default: boolean;
order_no: number;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
/**
* 탭 순서 변경 요청
* PUT /v1/item-master/custom-tabs/reorder
*/
export interface TabReorderRequest {
tab_orders: Array<{
id: number;
order_no: number;
}>;
}
/**
* 탭 컬럼 설정 업데이트 요청
* PUT /v1/item-master/custom-tabs/{id}/columns
*/
export interface TabColumnUpdateRequest {
columns: Array<{
key: string;
label: string;
visible: boolean;
order: number;
}>;
}
/**
* 탭 컬럼 응답
*/
export interface TabColumnResponse {
key: string;
label: string;
visible: boolean;
order: number;
}
// ============================================
// 단위 옵션
// ============================================
/**
* 단위 옵션 생성/수정 요청
* POST /v1/item-master/units
*/
export interface UnitOptionRequest {
unit_code: string;
unit_name: string;
description?: string;
is_active?: boolean;
}
/**
* 단위 옵션 응답
*/
export interface UnitOptionResponse {
id: number;
tenant_id: number;
unit_code: string;
unit_name: string;
description?: string;
is_active: boolean;
created_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 재질 옵션
// ============================================
export type MaterialType = 'STEEL' | 'ALUMINUM' | 'PLASTIC' | 'OTHER';
/**
* 재질 옵션 생성/수정 요청
*/
export interface MaterialOptionRequest {
material_code: string;
material_name: string;
material_type: MaterialType;
thickness?: string;
description?: string;
is_active?: boolean;
}
/**
* 재질 옵션 응답
*/
export interface MaterialOptionResponse {
id: number;
tenant_id: number;
material_code: string;
material_name: string;
material_type: MaterialType;
thickness?: string;
description?: string;
is_active: boolean;
created_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 표면처리 옵션
// ============================================
export type TreatmentType = 'PAINTING' | 'COATING' | 'PLATING' | 'NONE';
/**
* 표면처리 옵션 생성/수정 요청
*/
export interface TreatmentOptionRequest {
treatment_code: string;
treatment_name: string;
treatment_type: TreatmentType;
description?: string;
is_active?: boolean;
}
/**
* 표면처리 옵션 응답
*/
export interface TreatmentOptionResponse {
id: number;
tenant_id: number;
treatment_code: string;
treatment_name: string;
treatment_type: TreatmentType;
description?: string;
is_active: boolean;
created_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 에러 타입
// ============================================
/**
* API 에러 응답
*/
export interface ApiErrorResponse {
success: false;
message: string;
errors?: Record<string, string[]>; // Validation 에러
}
/**
* API 에러 클래스용 타입
*/
export interface ApiErrorData {
status: number;
message: string;
errors?: Record<string, string[]>;
}