서비스 레이어 리팩토링: - services/ 폴더 생성 (fieldService, masterFieldService, sectionService, pageService, templateService, attributeService) - 도메인 로직 중앙화 (validation, parsing, transform) - hooks와 dialogs에서 서비스 호출로 변경 버그 수정: - 섹션탭 실시간 동기화 문제 수정 (sectionsAsTemplates 중복 제거 순서 변경) - 422 Validation Error 수정 (createIndependentField → addFieldToSection) - 페이지 삭제 시 섹션-필드 연결 유지 (refreshIndependentSections 대신 직접 이동) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
4.9 KiB
TypeScript
194 lines
4.9 KiB
TypeScript
/**
|
|
* Page Service
|
|
* 페이지 관련 도메인 로직 중앙화
|
|
* - validation
|
|
* - path generation
|
|
* - transform (폼 ↔ API)
|
|
* - defaults
|
|
*/
|
|
|
|
import type { ItemPage } from '@/contexts/ItemMasterContext';
|
|
|
|
// ===== Types =====
|
|
|
|
export type ItemType = 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
|
|
|
|
export interface PageFormData {
|
|
pageName: string;
|
|
itemType: ItemType;
|
|
absolutePath?: string;
|
|
}
|
|
|
|
export interface PageValidationResult {
|
|
valid: boolean;
|
|
errors: {
|
|
page_name?: string;
|
|
item_type?: string;
|
|
absolute_path?: string;
|
|
};
|
|
}
|
|
|
|
// ===== Constants =====
|
|
|
|
const ITEM_TYPE_MAP: Record<ItemType, string> = {
|
|
'FG': '제품관리',
|
|
'PT': '부품관리',
|
|
'SM': '부자재관리',
|
|
'RM': '원자재관리',
|
|
'CS': '소모품관리',
|
|
};
|
|
|
|
// ===== Service =====
|
|
|
|
export const pageService = {
|
|
// ===== Validation =====
|
|
|
|
/**
|
|
* 전체 페이지 폼 유효성 검사
|
|
*/
|
|
validate: (data: Partial<PageFormData>): PageValidationResult => {
|
|
const errors: PageValidationResult['errors'] = {};
|
|
|
|
const nameValidation = pageService.validatePageName(data.pageName || '');
|
|
if (!nameValidation.valid) {
|
|
errors.page_name = nameValidation.error;
|
|
}
|
|
|
|
const typeValidation = pageService.validateItemType(data.itemType || '');
|
|
if (!typeValidation.valid) {
|
|
errors.item_type = typeValidation.error;
|
|
}
|
|
|
|
return {
|
|
valid: Object.keys(errors).length === 0,
|
|
errors,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 페이지명 유효성 검사
|
|
*/
|
|
validatePageName: (name: string): { valid: boolean; error?: string } => {
|
|
if (!name || !name.trim()) {
|
|
return { valid: false, error: '페이지명을 입력해주세요' };
|
|
}
|
|
return { valid: true };
|
|
},
|
|
|
|
/**
|
|
* 품목 타입 유효성 검사
|
|
*/
|
|
validateItemType: (type: string): { valid: boolean; error?: string } => {
|
|
const validTypes: ItemType[] = ['FG', 'PT', 'SM', 'RM', 'CS'];
|
|
if (!validTypes.includes(type as ItemType)) {
|
|
return { valid: false, error: '유효하지 않은 품목 타입입니다' };
|
|
}
|
|
return { valid: true };
|
|
},
|
|
|
|
/**
|
|
* 절대경로 유효성 검사
|
|
*/
|
|
validateAbsolutePath: (path: string): { valid: boolean; error?: string } => {
|
|
if (!path || !path.trim()) {
|
|
return { valid: false, error: '절대경로를 입력해주세요' };
|
|
}
|
|
if (!path.startsWith('/')) {
|
|
return { valid: false, error: '절대경로는 /로 시작해야 합니다' };
|
|
}
|
|
return { valid: true };
|
|
},
|
|
|
|
// ===== Path Generation =====
|
|
|
|
/**
|
|
* 품목 타입과 페이지명으로 절대 경로 생성
|
|
*/
|
|
generateAbsolutePath: (itemType: ItemType, pageName: string): string => {
|
|
const category = ITEM_TYPE_MAP[itemType] || '기타';
|
|
return `/${category}/${pageName}`;
|
|
},
|
|
|
|
/**
|
|
* 품목 타입 코드를 한글 카테고리명으로 변환
|
|
*/
|
|
getItemTypeLabel: (itemType: ItemType): string => {
|
|
return ITEM_TYPE_MAP[itemType] || '기타';
|
|
},
|
|
|
|
// ===== Transform =====
|
|
|
|
/**
|
|
* 폼 데이터 → API 요청 객체 변환
|
|
*/
|
|
toApiRequest: (
|
|
formData: PageFormData
|
|
): Omit<ItemPage, 'id' | 'tenant_id' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by'> => {
|
|
const absolutePath = formData.absolutePath ||
|
|
pageService.generateAbsolutePath(formData.itemType, formData.pageName);
|
|
|
|
return {
|
|
page_name: formData.pageName,
|
|
item_type: formData.itemType,
|
|
absolute_path: absolutePath,
|
|
is_active: true,
|
|
sections: [],
|
|
order_no: 0,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* ItemPage → 폼 데이터 변환
|
|
*/
|
|
toFormData: (page: ItemPage): PageFormData => {
|
|
return {
|
|
pageName: page.page_name,
|
|
itemType: page.item_type as ItemType,
|
|
absolutePath: page.absolute_path,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 페이지 복제용 데이터 생성
|
|
*/
|
|
toDuplicateRequest: (
|
|
originalPage: ItemPage
|
|
): Omit<ItemPage, 'id' | 'tenant_id' | 'created_at' | 'updated_at' | 'created_by' | 'updated_by'> => {
|
|
const duplicatedName = `${originalPage.page_name} (복제)`;
|
|
const absolutePath = pageService.generateAbsolutePath(
|
|
originalPage.item_type as ItemType,
|
|
duplicatedName
|
|
);
|
|
|
|
return {
|
|
page_name: duplicatedName,
|
|
item_type: originalPage.item_type,
|
|
absolute_path: absolutePath,
|
|
is_active: true,
|
|
sections: [], // 섹션은 별도 API로 복제
|
|
order_no: 0,
|
|
};
|
|
},
|
|
|
|
// ===== Defaults =====
|
|
|
|
/**
|
|
* 새 페이지 생성 시 기본값
|
|
*/
|
|
getDefaultFormData: (): PageFormData => ({
|
|
pageName: '',
|
|
itemType: 'FG',
|
|
}),
|
|
|
|
/**
|
|
* 지원하는 품목 타입 목록
|
|
*/
|
|
itemTypes: [
|
|
{ value: 'FG', label: '제품관리', description: '완제품' },
|
|
{ value: 'PT', label: '부품관리', description: '조립 부품' },
|
|
{ value: 'SM', label: '부자재관리', description: '부자재' },
|
|
{ value: 'RM', label: '원자재관리', description: '원자재' },
|
|
{ value: 'CS', label: '소모품관리', description: '소모품' },
|
|
] as const,
|
|
};
|