[feat]: Item Master 데이터 관리 기능 구현 및 타입 에러 수정
- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리) - HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가) - API 클라이언트 구현 (item-master.ts, 13개 엔드포인트) - ItemMasterContext 구현 (상태 관리 및 데이터 흐름) - 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등) - SSR 호환성 수정 (navigator API typeof window 체크) - 미사용 변수 ESLint 에러 해결 - Context 리팩토링 (AuthContext, RootProvider 추가) - API 유틸리티 추가 (error-handler, logger, transformers) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
421
src/lib/api/transformers.ts
Normal file
421
src/lib/api/transformers.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
// API 응답 데이터 변환 헬퍼
|
||||
// API 응답 (snake_case + 특정 값) ↔ Frontend State (snake_case + 변환된 값)
|
||||
|
||||
import type {
|
||||
ItemPageResponse,
|
||||
ItemSectionResponse,
|
||||
ItemFieldResponse,
|
||||
BomItemResponse,
|
||||
SectionTemplateResponse,
|
||||
MasterFieldResponse,
|
||||
UnitOptionResponse,
|
||||
CustomTabResponse,
|
||||
} from '@/types/item-master-api';
|
||||
|
||||
import type {
|
||||
ItemPage,
|
||||
ItemSection,
|
||||
ItemField,
|
||||
BOMItem,
|
||||
SectionTemplate,
|
||||
ItemMasterField,
|
||||
} from '@/contexts/ItemMasterContext';
|
||||
|
||||
// ============================================
|
||||
// 타입 값 변환 매핑
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* API section type → Frontend section_type 변환
|
||||
* API: 'fields' | 'bom'
|
||||
* Frontend: 'BASIC' | 'BOM' | 'CUSTOM'
|
||||
*/
|
||||
const SECTION_TYPE_MAP: Record<string, 'BASIC' | 'BOM' | 'CUSTOM'> = {
|
||||
fields: 'BASIC',
|
||||
bom: 'BOM',
|
||||
};
|
||||
|
||||
/**
|
||||
* Frontend section_type → API section type 변환
|
||||
*/
|
||||
const SECTION_TYPE_REVERSE_MAP: Record<string, 'fields' | 'bom'> = {
|
||||
BASIC: 'fields',
|
||||
BOM: 'bom',
|
||||
CUSTOM: 'fields', // CUSTOM은 fields로 매핑
|
||||
};
|
||||
|
||||
/**
|
||||
* API field_type → Frontend field_type 변환
|
||||
* API: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
|
||||
* Frontend: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX'
|
||||
*/
|
||||
const FIELD_TYPE_MAP: Record<
|
||||
string,
|
||||
'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX'
|
||||
> = {
|
||||
textbox: 'TEXT',
|
||||
number: 'NUMBER',
|
||||
dropdown: 'SELECT',
|
||||
checkbox: 'CHECKBOX',
|
||||
date: 'DATE',
|
||||
textarea: 'TEXTAREA',
|
||||
};
|
||||
|
||||
/**
|
||||
* Frontend field_type → API field_type 변환
|
||||
*/
|
||||
const FIELD_TYPE_REVERSE_MAP: Record<
|
||||
string,
|
||||
'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
|
||||
> = {
|
||||
TEXT: 'textbox',
|
||||
NUMBER: 'number',
|
||||
SELECT: 'dropdown',
|
||||
CHECKBOX: 'checkbox',
|
||||
DATE: 'date',
|
||||
TEXTAREA: 'textarea',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// API Response → Frontend State 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ItemPageResponse → ItemPage 변환
|
||||
*/
|
||||
export const transformPageResponse = (
|
||||
response: ItemPageResponse
|
||||
): ItemPage => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
page_name: response.page_name,
|
||||
item_type: response.item_type as 'FG' | 'PT' | 'SM' | 'RM' | 'CS',
|
||||
absolute_path: response.absolute_path,
|
||||
is_active: response.is_active,
|
||||
sections: response.sections?.map(transformSectionResponse) || [],
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemSectionResponse → ItemSection 변환
|
||||
* 주요 변환: type → section_type, 값 변환 (fields → BASIC, bom → BOM)
|
||||
*/
|
||||
export const transformSectionResponse = (
|
||||
response: ItemSectionResponse
|
||||
): ItemSection => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
page_id: response.page_id,
|
||||
title: response.title,
|
||||
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
|
||||
order_no: response.order_no,
|
||||
fields: response.fields?.map(transformFieldResponse) || [],
|
||||
bom_items: response.bomItems?.map(transformBomItemResponse) || [],
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemFieldResponse → ItemField 변환
|
||||
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
|
||||
*/
|
||||
export const transformFieldResponse = (
|
||||
response: ItemFieldResponse
|
||||
): ItemField => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
section_id: response.section_id,
|
||||
field_name: response.field_name,
|
||||
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
|
||||
order_no: response.order_no,
|
||||
is_required: response.is_required,
|
||||
placeholder: response.placeholder,
|
||||
default_value: response.default_value,
|
||||
display_condition: response.display_condition,
|
||||
validation_rules: response.validation_rules,
|
||||
options: response.options,
|
||||
properties: response.properties,
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* BomItemResponse → BOMItem 변환
|
||||
*/
|
||||
export const transformBomItemResponse = (
|
||||
response: BomItemResponse
|
||||
): BOMItem => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
section_id: response.section_id,
|
||||
item_code: response.item_code,
|
||||
item_name: response.item_name,
|
||||
quantity: response.quantity,
|
||||
unit: response.unit,
|
||||
unit_price: response.unit_price,
|
||||
total_price: response.total_price,
|
||||
spec: response.spec,
|
||||
note: response.note,
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* SectionTemplateResponse → SectionTemplate 변환
|
||||
* 주요 변환: title → template_name, type → section_type, 값 변환
|
||||
*/
|
||||
export const transformSectionTemplateResponse = (
|
||||
response: SectionTemplateResponse
|
||||
): SectionTemplate => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
template_name: response.title, // 필드명 변환
|
||||
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
|
||||
description: response.description,
|
||||
default_fields: null, // API 응답에 없으므로 null
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MasterFieldResponse → ItemMasterField 변환
|
||||
* 주요 변환: field_type 값 변환 (textbox → TEXT, dropdown → SELECT 등)
|
||||
*/
|
||||
export const transformMasterFieldResponse = (
|
||||
response: MasterFieldResponse
|
||||
): ItemMasterField => {
|
||||
return {
|
||||
id: response.id,
|
||||
tenant_id: response.tenant_id,
|
||||
field_name: response.field_name,
|
||||
field_type: FIELD_TYPE_MAP[response.field_type] || 'TEXT', // 타입 값 변환
|
||||
category: response.category,
|
||||
description: response.description,
|
||||
default_validation: response.validation_rules, // 필드명 매핑
|
||||
default_properties: response.properties, // 필드명 매핑
|
||||
created_by: response.created_by,
|
||||
updated_by: response.updated_by,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Frontend State → API Request 변환
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ItemSection → ItemSectionRequest 변환
|
||||
* 주요 변환: section_type → type, 값 역변환 (BASIC → fields, BOM → bom)
|
||||
*/
|
||||
export const transformSectionToRequest = (
|
||||
section: Partial<ItemSection>
|
||||
): { title: string; type: 'fields' | 'bom' } => {
|
||||
return {
|
||||
title: section.title || '',
|
||||
type: section.section_type
|
||||
? SECTION_TYPE_REVERSE_MAP[section.section_type] || 'fields'
|
||||
: 'fields',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemField → ItemFieldRequest 변환
|
||||
* 주요 변환: field_type 값 역변환 (TEXT → textbox, SELECT → dropdown 등)
|
||||
*/
|
||||
export const transformFieldToRequest = (field: Partial<ItemField>) => {
|
||||
return {
|
||||
field_name: field.field_name || '',
|
||||
field_type: field.field_type
|
||||
? FIELD_TYPE_REVERSE_MAP[field.field_type] || 'textbox'
|
||||
: 'textbox',
|
||||
is_required: field.is_required ?? false,
|
||||
placeholder: field.placeholder || null,
|
||||
default_value: field.default_value || null,
|
||||
display_condition: field.display_condition || null,
|
||||
validation_rules: field.validation_rules || null,
|
||||
options: field.options || null,
|
||||
properties: field.properties || null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* BOMItem → BomItemRequest 변환
|
||||
*/
|
||||
export const transformBomItemToRequest = (bomItem: Partial<BOMItem>) => {
|
||||
return {
|
||||
item_code: bomItem.item_code || undefined,
|
||||
item_name: bomItem.item_name || '',
|
||||
quantity: bomItem.quantity || 0,
|
||||
unit: bomItem.unit || undefined,
|
||||
unit_price: bomItem.unit_price || undefined,
|
||||
total_price: bomItem.total_price || undefined,
|
||||
spec: bomItem.spec || undefined,
|
||||
note: bomItem.note || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* SectionTemplate → SectionTemplateRequest 변환
|
||||
* 주요 변환: template_name → title, section_type → type, 값 역변환
|
||||
*/
|
||||
export const transformSectionTemplateToRequest = (
|
||||
template: Partial<SectionTemplate>
|
||||
) => {
|
||||
return {
|
||||
title: template.template_name || '', // 필드명 역변환
|
||||
type: template.section_type
|
||||
? SECTION_TYPE_REVERSE_MAP[template.section_type] || 'fields'
|
||||
: 'fields',
|
||||
description: template.description || undefined,
|
||||
is_default: false, // 기본값
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ItemMasterField → MasterFieldRequest 변환
|
||||
* 주요 변환: field_type 값 역변환, default_validation/properties 필드명 변환
|
||||
*/
|
||||
export const transformMasterFieldToRequest = (
|
||||
field: Partial<ItemMasterField>
|
||||
) => {
|
||||
return {
|
||||
field_name: field.field_name || '',
|
||||
field_type: field.field_type
|
||||
? FIELD_TYPE_REVERSE_MAP[field.field_type] || 'textbox'
|
||||
: 'textbox',
|
||||
category: field.category || undefined,
|
||||
description: field.description || undefined,
|
||||
is_common: false, // 기본값
|
||||
default_value: undefined,
|
||||
options: undefined,
|
||||
validation_rules: field.default_validation || undefined, // 필드명 역변환
|
||||
properties: field.default_properties || undefined, // 필드명 역변환
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 배치 변환 헬퍼
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 여러 페이지 응답을 한번에 변환
|
||||
*/
|
||||
export const transformPagesResponse = (
|
||||
responses: ItemPageResponse[]
|
||||
): ItemPage[] => {
|
||||
return responses.map(transformPageResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 섹션 응답을 한번에 변환
|
||||
*/
|
||||
export const transformSectionsResponse = (
|
||||
responses: ItemSectionResponse[]
|
||||
): ItemSection[] => {
|
||||
return responses.map(transformSectionResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 필드 응답을 한번에 변환
|
||||
*/
|
||||
export const transformFieldsResponse = (
|
||||
responses: ItemFieldResponse[]
|
||||
): ItemField[] => {
|
||||
return responses.map(transformFieldResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 BOM 아이템 응답을 한번에 변환
|
||||
*/
|
||||
export const transformBomItemsResponse = (
|
||||
responses: BomItemResponse[]
|
||||
): BOMItem[] => {
|
||||
return responses.map(transformBomItemResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 섹션 템플릿 응답을 한번에 변환
|
||||
*/
|
||||
export const transformSectionTemplatesResponse = (
|
||||
responses: SectionTemplateResponse[]
|
||||
): SectionTemplate[] => {
|
||||
return responses.map(transformSectionTemplateResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 마스터 필드 응답을 한번에 변환
|
||||
*/
|
||||
export const transformMasterFieldsResponse = (
|
||||
responses: MasterFieldResponse[]
|
||||
): ItemMasterField[] => {
|
||||
return responses.map(transformMasterFieldResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* UnitOptionResponse → MasterOption 변환 (Frontend의 MasterOption 타입에 맞춤)
|
||||
*/
|
||||
export const transformUnitOptionResponse = (
|
||||
response: UnitOptionResponse
|
||||
): { id: string; value: string; label: string; isActive: boolean } => {
|
||||
return {
|
||||
id: response.id.toString(), // number → string 변환
|
||||
value: response.value,
|
||||
label: response.label,
|
||||
isActive: true, // API에 없으므로 기본값
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* CustomTabResponse → Frontend customTabs 타입 변환
|
||||
*/
|
||||
export const transformCustomTabResponse = (
|
||||
response: CustomTabResponse
|
||||
): { id: string; label: string; icon: string; isDefault: boolean; order: number } => {
|
||||
return {
|
||||
id: response.id.toString(), // number → string 변환
|
||||
label: response.label,
|
||||
icon: response.icon || 'FileText', // null이면 기본 아이콘
|
||||
isDefault: response.is_default,
|
||||
order: response.order_no,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 단위 옵션 응답을 한번에 변환
|
||||
*/
|
||||
export const transformUnitOptionsResponse = (
|
||||
responses: UnitOptionResponse[]
|
||||
) => {
|
||||
return responses.map(transformUnitOptionResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 커스텀 탭 응답을 한번에 변환
|
||||
*/
|
||||
export const transformCustomTabsResponse = (
|
||||
responses: CustomTabResponse[]
|
||||
) => {
|
||||
return responses.map(transformCustomTabResponse);
|
||||
};
|
||||
Reference in New Issue
Block a user