- field_key 저장 시 백엔드 형식({ID}_{사용자입력})으로 전송
- API 요청 전 field_key 유효성 검증 추가
- 계층구조 탭 필드 추가/수정 시 field_key 반영
- 섹션 탭에서 field_key 표시 시 사용자입력 부분만 추출
- sectionsAsTemplates useMemo에서 linkedSections/unlinkedSections 모두 수정
- 마스터 필드, 템플릿 필드 다이얼로그에서 field_key 입력 지원
- ItemMasterContext에 field_key 상태 업데이트 로직 추가
- transformers에서 field_key 변환 처리
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
// 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로 매핑
|
|
};
|
|
|
|
// 2025-11-26: field_type은 API와 Frontend가 동일한 값을 사용하므로 변환 불필요
|
|
// API & Frontend: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea'
|
|
type FieldType = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
|
|
|
/**
|
|
* field_type 기본값 반환 (알 수 없는 값일 경우)
|
|
*/
|
|
const getFieldType = (type: string): FieldType => {
|
|
const validTypes: FieldType[] = ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'];
|
|
return validTypes.includes(type as FieldType) ? (type as FieldType) : 'textbox';
|
|
};
|
|
|
|
// ============================================
|
|
// 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',
|
|
description: response.description ?? null, // 2025-11-26 추가
|
|
absolute_path: response.absolute_path || '', // null일 경우 빈 문자열
|
|
is_active: response.is_active,
|
|
order_no: response.order_no ?? 0, // 2025-11-26 추가
|
|
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)
|
|
* 2025-11-26: group_id, is_template, is_default, description 추가 (section_templates 통합)
|
|
*/
|
|
export const transformSectionResponse = (
|
|
response: ItemSectionResponse
|
|
): ItemSection => {
|
|
return {
|
|
id: response.id,
|
|
tenant_id: response.tenant_id,
|
|
group_id: response.group_id, // 2025-11-26 추가: 독립 섹션 그룹화용
|
|
page_id: response.page_id, // null이면 독립 섹션
|
|
title: response.title,
|
|
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // 타입 값 변환
|
|
description: response.description, // 2025-11-26 추가
|
|
order_no: response.order_no,
|
|
is_template: response.is_template, // 2025-11-26 추가: 템플릿 여부 (section_templates 통합)
|
|
is_default: response.is_default, // 2025-11-26 추가: 기본 템플릿 여부
|
|
fields: response.fields?.map(transformFieldResponse) || [],
|
|
// 2025-11-27: 백엔드가 bom_items (snake_case)로 반환하므로 둘 다 체크
|
|
bom_items: (response.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 변환
|
|
* 2025-11-26: field_type은 API와 동일한 값 사용 (변환 불필요)
|
|
*/
|
|
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_key: response.field_key, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력})
|
|
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
|
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 변환
|
|
* 2025-11-26: field_type은 API와 동일한 값 사용, 속성명도 API와 통일
|
|
*/
|
|
export const transformMasterFieldResponse = (
|
|
response: MasterFieldResponse
|
|
): ItemMasterField => {
|
|
return {
|
|
id: response.id,
|
|
tenant_id: response.tenant_id,
|
|
field_name: response.field_name,
|
|
field_key: response.field_key ?? null, // 2025-11-28: field_key 추가 (형식: {ID}_{사용자입력})
|
|
field_type: getFieldType(response.field_type), // API와 동일한 타입
|
|
category: response.category,
|
|
description: response.description,
|
|
is_common: response.is_common ?? false, // 공통 필드 여부
|
|
default_value: response.default_value ?? null, // 기본값
|
|
options: response.options ?? null, // dropdown 옵션
|
|
validation_rules: response.validation_rules ?? null, // 검증 규칙
|
|
properties: response.properties ?? null, // 추가 속성
|
|
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 변환
|
|
* 2025-11-26: field_type은 API와 동일한 값 사용 (변환 불필요)
|
|
*/
|
|
export const transformFieldToRequest = (field: Partial<ItemField>) => {
|
|
return {
|
|
field_name: field.field_name || '',
|
|
field_type: field.field_type || 'textbox', // API와 동일한 타입
|
|
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 변환
|
|
* 2025-11-26: field_type과 속성명 모두 API와 동일하게 통일
|
|
*/
|
|
export const transformMasterFieldToRequest = (
|
|
field: Partial<ItemMasterField>
|
|
) => {
|
|
return {
|
|
field_name: field.field_name || '',
|
|
field_type: field.field_type || 'textbox', // API와 동일한 타입
|
|
category: field.category || undefined,
|
|
description: field.description || undefined,
|
|
is_common: field.is_common ?? false,
|
|
default_value: field.default_value || undefined,
|
|
options: field.options || undefined,
|
|
validation_rules: field.validation_rules || undefined, // API와 동일
|
|
properties: field.properties || undefined, // API와 동일
|
|
};
|
|
};
|
|
|
|
// ============================================
|
|
// 배치 변환 헬퍼
|
|
// ============================================
|
|
|
|
/**
|
|
* 여러 페이지 응답을 한번에 변환
|
|
*/
|
|
export const transformPagesResponse = (
|
|
responses: ItemPageResponse[] | undefined | null
|
|
): ItemPage[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformPageResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 섹션 응답을 한번에 변환
|
|
*/
|
|
export const transformSectionsResponse = (
|
|
responses: ItemSectionResponse[] | undefined | null
|
|
): ItemSection[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformSectionResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 필드 응답을 한번에 변환
|
|
*/
|
|
export const transformFieldsResponse = (
|
|
responses: ItemFieldResponse[] | undefined | null
|
|
): ItemField[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformFieldResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 BOM 아이템 응답을 한번에 변환
|
|
*/
|
|
export const transformBomItemsResponse = (
|
|
responses: BomItemResponse[] | undefined | null
|
|
): BOMItem[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformBomItemResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 섹션 템플릿 응답을 한번에 변환
|
|
*/
|
|
export const transformSectionTemplatesResponse = (
|
|
responses: SectionTemplateResponse[] | undefined | null
|
|
): SectionTemplate[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformSectionTemplateResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 마스터 필드 응답을 한번에 변환
|
|
*/
|
|
export const transformMasterFieldsResponse = (
|
|
responses: MasterFieldResponse[] | undefined | null
|
|
): ItemMasterField[] => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
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[] | undefined | null
|
|
) => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformUnitOptionResponse);
|
|
};
|
|
|
|
/**
|
|
* 여러 커스텀 탭 응답을 한번에 변환
|
|
*/
|
|
export const transformCustomTabsResponse = (
|
|
responses: CustomTabResponse[] | undefined | null
|
|
) => {
|
|
if (!responses || !Array.isArray(responses)) return [];
|
|
return responses.map(transformCustomTabResponse);
|
|
};
|
|
|
|
/**
|
|
* ItemSectionResponse → SectionTemplate 변환
|
|
* 2025-11-26: 백엔드가 sectionTemplates 대신 sections를 반환하는 경우 사용
|
|
* is_template=true인 섹션을 SectionTemplate 형식으로 변환
|
|
*/
|
|
export const transformSectionTemplateFromSection = (
|
|
response: ItemSectionResponse
|
|
): SectionTemplate => {
|
|
return {
|
|
id: response.id,
|
|
tenant_id: response.tenant_id,
|
|
template_name: response.title, // title → template_name
|
|
section_type: SECTION_TYPE_MAP[response.type] || 'BASIC', // type → section_type
|
|
description: response.description,
|
|
default_fields: null, // API 응답에 없으므로 null
|
|
// 필드 변환은 별도 처리 필요 (fields가 있으면 TemplateField로 변환)
|
|
// 2025-11-28: fieldKey를 실제 field_key 사용하도록 수정 (기존: field_name에서 생성)
|
|
fields: response.fields?.map(field => ({
|
|
id: field.id.toString(),
|
|
name: field.field_name,
|
|
fieldKey: field.field_key || field.field_name.toLowerCase().replace(/\s+/g, '_'),
|
|
property: {
|
|
inputType: getFieldType(field.field_type),
|
|
required: field.is_required,
|
|
options: field.options?.map((opt: { label: string; value: string }) => opt.label || opt.value),
|
|
},
|
|
description: field.placeholder || undefined,
|
|
})),
|
|
bomItems: response.bomItems?.map(transformBomItemResponse),
|
|
created_by: response.created_by,
|
|
updated_by: response.updated_by,
|
|
created_at: response.created_at,
|
|
updated_at: response.updated_at,
|
|
};
|
|
}; |