refactor: 품목기준관리 서비스 레이어 도입 및 버그 수정
서비스 레이어 리팩토링: - 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>
This commit is contained in:
@@ -13,6 +13,7 @@ import { Plus, X, Edit, Trash2, Check } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { ItemPage, ItemSection, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { ConditionalDisplayUI, type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
|
||||
import { fieldService } from '../services';
|
||||
|
||||
// 입력 타입 정의
|
||||
export type InputType = 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
@@ -124,12 +125,12 @@ export function FieldDialog({
|
||||
}: FieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !newFieldName.trim();
|
||||
// fieldService를 사용한 유효성 검사
|
||||
const nameValidation = fieldService.validateFieldName(newFieldName);
|
||||
const keyValidation = fieldService.validateFieldKey(newFieldKey);
|
||||
const isNameEmpty = !nameValidation.valid;
|
||||
const isKeyEmpty = !newFieldKey.trim();
|
||||
// 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용
|
||||
const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||
const isKeyInvalid = newFieldKey.trim() !== '' && !fieldKeyPattern.test(newFieldKey);
|
||||
const isKeyInvalid = newFieldKey.trim() !== '' && !keyValidation.valid;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsSubmitted(false);
|
||||
|
||||
@@ -9,15 +9,10 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { masterFieldService } from '../services';
|
||||
|
||||
const INPUT_TYPE_OPTIONS = [
|
||||
{ value: 'textbox', label: '텍스트 입력' },
|
||||
{ value: 'number', label: '숫자 입력' },
|
||||
{ value: 'dropdown', label: '드롭다운' },
|
||||
{ value: 'checkbox', label: '체크박스' },
|
||||
{ value: 'date', label: '날짜' },
|
||||
{ value: 'textarea', label: '긴 텍스트' },
|
||||
];
|
||||
// 2025-12-01: masterFieldService.fieldTypes 재사용으로 리팩토링
|
||||
const INPUT_TYPE_OPTIONS = masterFieldService.fieldTypes;
|
||||
|
||||
interface MasterFieldDialogProps {
|
||||
isMasterFieldDialogOpen: boolean;
|
||||
@@ -82,12 +77,12 @@ export function MasterFieldDialog({
|
||||
}: MasterFieldDialogProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// 유효성 검사
|
||||
const isNameEmpty = !newMasterFieldName.trim();
|
||||
// 2025-12-01: masterFieldService 사용으로 유효성 검사 중앙화
|
||||
const nameValidation = masterFieldService.validateFieldName(newMasterFieldName);
|
||||
const keyValidation = masterFieldService.validateFieldKey(newMasterFieldKey);
|
||||
const isNameEmpty = !nameValidation.valid;
|
||||
const isKeyEmpty = !newMasterFieldKey.trim();
|
||||
// 2025-11-28: field_key validation - 영문 시작, 영문+숫자+언더스코어만 허용
|
||||
const fieldKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||
const isKeyInvalid = newMasterFieldKey.trim() !== '' && !fieldKeyPattern.test(newMasterFieldKey);
|
||||
const isKeyInvalid = newMasterFieldKey.trim() !== '' && !keyValidation.valid;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsMasterFieldDialogOpen(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { MasterOption, OptionColumn } from '../types';
|
||||
import { attributeService } from '../services';
|
||||
|
||||
export interface UseAttributeManagementReturn {
|
||||
// 속성 옵션 상태
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemField, ItemMasterField, FieldDisplayCondition } from '@/contexts/ItemMasterContext';
|
||||
import { type ConditionalFieldConfig } from '../components/ConditionalDisplayUI';
|
||||
import { fieldService } from '../services';
|
||||
|
||||
export interface UseFieldManagementReturn {
|
||||
// 다이얼로그 상태
|
||||
@@ -230,19 +231,15 @@ export function useFieldManagement(): UseFieldManagementReturn {
|
||||
const handleEditField = (sectionId: string, field: ItemField) => {
|
||||
setSelectedSectionForField(Number(sectionId));
|
||||
setEditingFieldId(field.id);
|
||||
setNewFieldName(field.field_name);
|
||||
// 2025-11-28: field_key 사용 (없으면 빈 문자열)
|
||||
// field_key 형식: {ID}_{사용자입력} → 사용자입력 부분만 추출해서 표시
|
||||
const fieldKeyValue = field.field_key || '';
|
||||
const userInputPart = fieldKeyValue.includes('_')
|
||||
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||
: fieldKeyValue;
|
||||
setNewFieldKey(userInputPart);
|
||||
setNewFieldInputType(field.field_type);
|
||||
// 2025-11-27: is_required와 properties.required 둘 다 체크
|
||||
setNewFieldRequired(field.is_required || field.properties?.required || false);
|
||||
setNewFieldOptions(field.options?.map(opt => opt.value).join(', ') || '');
|
||||
setNewFieldDescription(field.placeholder || '');
|
||||
|
||||
// fieldService를 사용하여 폼 데이터 변환
|
||||
const formData = fieldService.toFormData(field);
|
||||
setNewFieldName(formData.name);
|
||||
setNewFieldKey(formData.key);
|
||||
setNewFieldInputType(formData.inputType);
|
||||
setNewFieldRequired(formData.required);
|
||||
setNewFieldOptions(formData.options);
|
||||
setNewFieldDescription(formData.description);
|
||||
|
||||
// 조건부 표시 설정 로드
|
||||
if (field.display_condition) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { masterFieldService } from '../services';
|
||||
|
||||
/**
|
||||
* @deprecated 2025-11-27: item_fields로 통합됨.
|
||||
@@ -111,25 +112,24 @@ export function useMasterFieldManagement(): UseMasterFieldManagementReturn {
|
||||
};
|
||||
|
||||
// 마스터 항목 수정 시작
|
||||
// 2025-11-28: field_key 추가 - {ID}_{사용자입력} 형식에서 사용자입력 부분만 추출
|
||||
// 2025-12-01: masterFieldService.toFormData() 사용으로 리팩토링
|
||||
const handleEditMasterField = (field: ItemMasterField) => {
|
||||
setEditingMasterFieldId(field.id);
|
||||
setNewMasterFieldName(field.field_name);
|
||||
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||
const fieldKeyValue = field.field_key || '';
|
||||
const userInputPart = fieldKeyValue.includes('_')
|
||||
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||
: fieldKeyValue;
|
||||
setNewMasterFieldKey(userInputPart);
|
||||
setNewMasterFieldInputType(field.field_type || 'textbox');
|
||||
setNewMasterFieldRequired((field.properties as any)?.required || false);
|
||||
setNewMasterFieldCategory(field.category || '공통');
|
||||
setNewMasterFieldDescription(field.description || '');
|
||||
setNewMasterFieldOptions(field.options?.map(o => o.label).join(', ') || '');
|
||||
setNewMasterFieldAttributeType((field.properties as any)?.attributeType || 'custom');
|
||||
setNewMasterFieldMultiColumn((field.properties as any)?.multiColumn || false);
|
||||
setNewMasterFieldColumnCount((field.properties as any)?.columnCount || 2);
|
||||
setNewMasterFieldColumnNames((field.properties as any)?.columnNames || ['컬럼1', '컬럼2']);
|
||||
|
||||
// masterFieldService를 사용하여 폼 데이터 변환
|
||||
const formData = masterFieldService.toFormData(field);
|
||||
setNewMasterFieldName(formData.name);
|
||||
setNewMasterFieldKey(formData.key);
|
||||
setNewMasterFieldInputType(formData.inputType);
|
||||
setNewMasterFieldRequired(formData.required);
|
||||
setNewMasterFieldCategory(formData.category);
|
||||
setNewMasterFieldDescription(formData.description);
|
||||
setNewMasterFieldOptions(formData.options);
|
||||
setNewMasterFieldAttributeType(formData.attributeType);
|
||||
setNewMasterFieldMultiColumn(formData.multiColumn);
|
||||
setNewMasterFieldColumnCount(formData.columnCount);
|
||||
setNewMasterFieldColumnNames(formData.columnNames);
|
||||
|
||||
setIsMasterFieldDialogOpen(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage } from '@/contexts/ItemMasterContext';
|
||||
import { ApiError, getErrorMessage } from '@/lib/api/error-handler';
|
||||
import { generateAbsolutePath } from '../utils/pathUtils';
|
||||
import { pageService } from '../services';
|
||||
|
||||
export interface UsePageManagementReturn {
|
||||
// 상태
|
||||
@@ -75,7 +75,7 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
|
||||
// 마이그레이션 실행 (한 번에 처리)
|
||||
pagesToMigrate.forEach(page => {
|
||||
const absolutePath = generateAbsolutePath(page.item_type, page.page_name);
|
||||
const absolutePath = pageService.generateAbsolutePath(page.item_type as 'FG' | 'PT' | 'SM' | 'RM' | 'CS', page.page_name);
|
||||
updateItemPage(page.id, { absolute_path: absolutePath });
|
||||
migrationDoneRef.current.add(page.id);
|
||||
});
|
||||
@@ -93,7 +93,7 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const absolutePath = generateAbsolutePath(newPageItemType, newPageName);
|
||||
const absolutePath = pageService.generateAbsolutePath(newPageItemType, newPageName);
|
||||
|
||||
const newPage = await addItemPage({
|
||||
page_name: newPageName,
|
||||
@@ -141,7 +141,7 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
setIsLoading(true);
|
||||
|
||||
const duplicatedPageName = `${originalPage.page_name} (복제)`;
|
||||
const absolutePath = generateAbsolutePath(originalPage.item_type, duplicatedPageName);
|
||||
const absolutePath = pageService.generateAbsolutePath(originalPage.item_type as 'FG' | 'PT' | 'SM' | 'RM' | 'CS', duplicatedPageName);
|
||||
|
||||
const newPage = await addItemPage({
|
||||
page_name: duplicatedPageName,
|
||||
@@ -166,10 +166,11 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
};
|
||||
|
||||
// 페이지 삭제
|
||||
// 2025-12-01: 페이지 삭제 시 섹션들은 독립 섹션으로 이동 (필드 연결 유지)
|
||||
const handleDeletePage = (pageId: number) => {
|
||||
const pageToDelete = itemPages.find(p => p.id === pageId);
|
||||
const sectionIds = pageToDelete?.sections.map(s => s.id) || [];
|
||||
const fieldIds = pageToDelete?.sections.flatMap(s => s.fields?.map(f => f.id) || []) || [];
|
||||
const sectionCount = pageToDelete?.sections.length || 0;
|
||||
const fieldCount = pageToDelete?.sections.flatMap(s => s.fields || []).length || 0;
|
||||
|
||||
deleteItemPage(pageId);
|
||||
|
||||
@@ -181,8 +182,8 @@ export function usePageManagement(): UsePageManagementReturn {
|
||||
|
||||
console.log('페이지 삭제 완료:', {
|
||||
pageId,
|
||||
removedSections: sectionIds.length,
|
||||
removedFields: fieldIds.length
|
||||
sectionsMovedToIndependent: sectionCount,
|
||||
fieldsPreserved: fieldCount
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, ItemSection, SectionTemplate } from '@/contexts/ItemMasterContext';
|
||||
import { sectionService } from '../services';
|
||||
|
||||
export interface UseSectionManagementReturn {
|
||||
// 상태
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useItemMaster } from '@/contexts/ItemMasterContext';
|
||||
import type { ItemPage, SectionTemplate, TemplateField, BOMItem, ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { templateService } from '../services';
|
||||
|
||||
export interface UseTemplateManagementReturn {
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -103,6 +104,12 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
independentFields,
|
||||
// 2025-11-27: 필드 수정 API
|
||||
updateField,
|
||||
// 2025-12-01: 섹션에 필드 추가 API (계층구조 탭과 동일한 방식)
|
||||
addFieldToSection,
|
||||
// 2025-12-01: BOM 관리 API (API 기반으로 변경)
|
||||
addBOMItem,
|
||||
updateBOMItem,
|
||||
deleteBOMItem,
|
||||
} = useItemMaster();
|
||||
|
||||
// 섹션 템플릿 다이얼로그 상태
|
||||
@@ -256,6 +263,7 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
// 템플릿 필드 추가/수정 (2025-11-27: API 사용으로 변경)
|
||||
// sectionsAsTemplates가 itemPages + independentSections에서 파생되므로
|
||||
// entity_relationships 기반 연결 API를 사용해야 실시간 반영됨
|
||||
// 2025-12-01: custom/master 모드 분기 처리 추가
|
||||
const handleAddTemplateField = async () => {
|
||||
if (!currentTemplateId || !templateFieldName.trim() || !templateFieldKey.trim()) {
|
||||
toast.error('모든 필수 항목을 입력해주세요');
|
||||
@@ -290,15 +298,51 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
return;
|
||||
}
|
||||
|
||||
// 추가 모드: 기존 필드를 섹션에 연결
|
||||
const existingField = independentFields.find(f => f.id.toString() === templateFieldKey);
|
||||
// 추가 모드: custom/master 모드에 따라 분기 처리
|
||||
// 2025-12-01: custom 모드에서는 새 필드 생성 후 연결, master 모드에서는 기존 필드 연결
|
||||
if (templateFieldInputMode === 'master') {
|
||||
// master 모드: 기존 필드를 섹션에 연결
|
||||
// templateFieldKey는 선택된 필드의 ID (handleSelectMasterField에서 설정됨)
|
||||
const existingField = independentFields.find(f => f.id.toString() === templateFieldKey);
|
||||
|
||||
if (existingField) {
|
||||
await linkFieldToSection(currentTemplateId, existingField.id);
|
||||
toast.success('항목이 섹션에 연결되었습니다');
|
||||
if (existingField) {
|
||||
await linkFieldToSection(currentTemplateId, existingField.id);
|
||||
toast.success('항목이 섹션에 연결되었습니다');
|
||||
} else {
|
||||
toast.error('항목 탭에서 먼저 항목을 생성해주세요');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
toast.error('항목 탭에서 먼저 항목을 생성해주세요');
|
||||
return;
|
||||
// custom 모드: 섹션에 직접 필드 추가 (계층구조 탭과 동일한 방식)
|
||||
// 2025-12-01: createIndependentField + linkFieldToSection 대신 addFieldToSection 사용
|
||||
// POST /sections/{id}/fields API를 사용하여 섹션에 바로 필드 생성
|
||||
const newFieldData = {
|
||||
section_id: currentTemplateId,
|
||||
master_field_id: null,
|
||||
field_name: templateFieldName,
|
||||
field_key: templateFieldKey,
|
||||
field_type: templateFieldInputType,
|
||||
order_no: 0,
|
||||
is_required: templateFieldRequired,
|
||||
placeholder: templateFieldDescription || null,
|
||||
default_value: null,
|
||||
display_condition: null,
|
||||
validation_rules: null,
|
||||
options: templateFieldInputType === 'dropdown' && templateFieldOptions.trim()
|
||||
? templateFieldOptions.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
properties: {
|
||||
inputType: templateFieldInputType,
|
||||
required: templateFieldRequired,
|
||||
multiColumn: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') ? templateFieldMultiColumn : undefined,
|
||||
columnCount: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnCount : undefined,
|
||||
columnNames: (templateFieldInputType === 'textbox' || templateFieldInputType === 'textarea') && templateFieldMultiColumn ? templateFieldColumnNames : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// 섹션에 필드 추가 (계층구조 탭과 동일)
|
||||
await addFieldToSection(currentTemplateId, newFieldData);
|
||||
toast.success('항목이 섹션에 추가되었습니다');
|
||||
}
|
||||
|
||||
resetTemplateFieldForm();
|
||||
@@ -314,12 +358,8 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
setCurrentTemplateId(templateId);
|
||||
setEditingTemplateFieldId(Number(field.id));
|
||||
setTemplateFieldName(field.name);
|
||||
// 2025-11-28: field_key 형식 {ID}_{사용자입력}에서 사용자입력 부분만 추출
|
||||
const fieldKeyValue = field.fieldKey || '';
|
||||
const userInputPart = fieldKeyValue.includes('_')
|
||||
? fieldKeyValue.substring(fieldKeyValue.indexOf('_') + 1)
|
||||
: fieldKeyValue;
|
||||
setTemplateFieldKey(userInputPart);
|
||||
// 2025-12-01: templateService 사용으로 변경
|
||||
setTemplateFieldKey(templateService.extractUserInputFromFieldKey(field.fieldKey || ''));
|
||||
setTemplateFieldInputType(field.property.inputType);
|
||||
setTemplateFieldRequired(field.property.required);
|
||||
setTemplateFieldOptions(field.property.options?.join(', ') || '');
|
||||
@@ -346,42 +386,41 @@ export function useTemplateManagement(): UseTemplateManagementReturn {
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 추가
|
||||
const handleAddBOMItemToTemplate = (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
||||
const newItem: BOMItem = {
|
||||
...item,
|
||||
id: Date.now(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tenant_id: tenantId ?? 0,
|
||||
section_id: 0
|
||||
};
|
||||
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template) return;
|
||||
|
||||
const updatedBomItems = [...(template.bomItems || []), newItem];
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 추가 (2025-12-01: API 기반으로 변경)
|
||||
// templateId = sectionId (sectionsAsTemplates에서 섹션 ID로 사용)
|
||||
const handleAddBOMItemToTemplate = async (templateId: number, item: Omit<BOMItem, 'id' | 'created_at' | 'updated_at' | 'tenant_id' | 'section_id'>) => {
|
||||
try {
|
||||
// addBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await addBOMItem(templateId, item);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 추가 실패:', error);
|
||||
toast.error('BOM 항목 추가에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 수정
|
||||
const handleUpdateBOMItemInTemplate = (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.map(bom =>
|
||||
bom.id === itemId ? { ...bom, ...item } : bom
|
||||
);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 수정 (2025-12-01: API 기반으로 변경)
|
||||
const handleUpdateBOMItemInTemplate = async (templateId: number, itemId: number, item: Partial<BOMItem>) => {
|
||||
try {
|
||||
// updateBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await updateBOMItem(itemId, item);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 수정 실패:', error);
|
||||
toast.error('BOM 항목 수정에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// BOM 항목 삭제
|
||||
const handleDeleteBOMItemFromTemplate = (templateId: number, itemId: number) => {
|
||||
const template = sectionTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.bomItems) return;
|
||||
|
||||
const updatedBomItems = template.bomItems.filter(bom => bom.id !== itemId);
|
||||
updateSectionTemplate(templateId, { bomItems: updatedBomItems });
|
||||
// BOM 항목 삭제 (2025-12-01: API 기반으로 변경)
|
||||
const handleDeleteBOMItemFromTemplate = async (templateId: number, itemId: number) => {
|
||||
try {
|
||||
// deleteBOMItem API 호출 (Context에서 itemPages/independentSections 자동 업데이트)
|
||||
await deleteBOMItem(itemId);
|
||||
// toast는 BOMManagementSection 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('BOM 항목 삭제 실패:', error);
|
||||
toast.error('BOM 항목 삭제에 실패했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
// 섹션 템플릿 폼 초기화
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Attribute Service
|
||||
* 속성(단위/재질/표면처리 등) 관련 도메인 로직 중앙화
|
||||
* - validation
|
||||
* - option management
|
||||
* - column management
|
||||
* - defaults
|
||||
*/
|
||||
|
||||
import type { MasterOption, OptionColumn } from '../types';
|
||||
|
||||
// ===== Types =====
|
||||
|
||||
export type AttributeType = 'unit' | 'material' | 'surface' | string;
|
||||
export type ColumnType = 'text' | 'number';
|
||||
|
||||
export interface OptionFormData {
|
||||
value: string;
|
||||
label: string;
|
||||
inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
required: boolean;
|
||||
options: string;
|
||||
placeholder: string;
|
||||
defaultValue: string;
|
||||
columnValues: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ColumnFormData {
|
||||
name: string;
|
||||
key: string;
|
||||
type: ColumnType;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface OptionValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
value?: string;
|
||||
label?: string;
|
||||
options?: string;
|
||||
columnValues?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ColumnValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
name?: string;
|
||||
key?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Service =====
|
||||
|
||||
export const attributeService = {
|
||||
// ===== Option Validation =====
|
||||
|
||||
/**
|
||||
* 옵션 폼 유효성 검사
|
||||
*/
|
||||
validateOption: (data: Partial<OptionFormData>, columns?: OptionColumn[]): OptionValidationResult => {
|
||||
const errors: OptionValidationResult['errors'] = {};
|
||||
|
||||
if (!data.value || !data.value.trim()) {
|
||||
errors.value = '값을 입력해주세요';
|
||||
}
|
||||
|
||||
if (!data.label || !data.label.trim()) {
|
||||
errors.label = '표시명을 입력해주세요';
|
||||
}
|
||||
|
||||
// 드롭다운일 경우 옵션 필수
|
||||
if (data.inputType === 'dropdown' && (!data.options || !data.options.trim())) {
|
||||
errors.options = '드롭다운 옵션을 입력해주세요';
|
||||
}
|
||||
|
||||
// 필수 칼럼 값 체크
|
||||
if (columns && data.columnValues) {
|
||||
const columnErrors: Record<string, string> = {};
|
||||
for (const column of columns) {
|
||||
if (column.required && !data.columnValues[column.key]?.trim()) {
|
||||
columnErrors[column.key] = `${column.name}은(는) 필수 입력 항목입니다`;
|
||||
}
|
||||
}
|
||||
if (Object.keys(columnErrors).length > 0) {
|
||||
errors.columnValues = columnErrors;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 값 유효성 검사
|
||||
*/
|
||||
validateOptionValue: (value: string): { valid: boolean; error?: string } => {
|
||||
if (!value || !value.trim()) {
|
||||
return { valid: false, error: '값을 입력해주세요' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 표시명 유효성 검사
|
||||
*/
|
||||
validateOptionLabel: (label: string): { valid: boolean; error?: string } => {
|
||||
if (!label || !label.trim()) {
|
||||
return { valid: false, error: '표시명을 입력해주세요' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
// ===== Column Validation =====
|
||||
|
||||
/**
|
||||
* 칼럼 폼 유효성 검사
|
||||
*/
|
||||
validateColumn: (data: Partial<ColumnFormData>): ColumnValidationResult => {
|
||||
const errors: ColumnValidationResult['errors'] = {};
|
||||
|
||||
if (!data.name || !data.name.trim()) {
|
||||
errors.name = '칼럼명을 입력해주세요';
|
||||
}
|
||||
|
||||
if (!data.key || !data.key.trim()) {
|
||||
errors.key = '칼럼 키를 입력해주세요';
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
// ===== Transform =====
|
||||
|
||||
/**
|
||||
* 폼 데이터 → MasterOption 변환
|
||||
*/
|
||||
toMasterOption: (formData: OptionFormData, attributeType: string): MasterOption => {
|
||||
return {
|
||||
id: `${attributeType}-${Date.now()}`,
|
||||
value: formData.value,
|
||||
label: formData.label,
|
||||
isActive: true,
|
||||
inputType: formData.inputType,
|
||||
required: formData.required,
|
||||
options: formData.inputType === 'dropdown'
|
||||
? formData.options.split(',').map(o => o.trim()).filter(o => o)
|
||||
: undefined,
|
||||
placeholder: formData.placeholder || undefined,
|
||||
defaultValue: formData.defaultValue || undefined,
|
||||
columnValues: Object.keys(formData.columnValues).length > 0
|
||||
? { ...formData.columnValues }
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 폼 데이터 → OptionColumn 변환
|
||||
*/
|
||||
toOptionColumn: (formData: ColumnFormData): OptionColumn => {
|
||||
return {
|
||||
id: `col-${Date.now()}`,
|
||||
key: formData.key,
|
||||
name: formData.name,
|
||||
type: formData.type,
|
||||
required: formData.required,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 배열에서 라벨 목록 추출
|
||||
*/
|
||||
extractLabels: (options: MasterOption[]): string[] => {
|
||||
return options.map(opt => opt.label);
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 배열에서 값 목록 추출
|
||||
*/
|
||||
extractValues: (options: MasterOption[]): string[] => {
|
||||
return options.map(opt => opt.value);
|
||||
},
|
||||
|
||||
// ===== Defaults =====
|
||||
|
||||
/**
|
||||
* 새 옵션 생성 시 기본값
|
||||
*/
|
||||
getDefaultOptionFormData: (): OptionFormData => ({
|
||||
value: '',
|
||||
label: '',
|
||||
inputType: 'textbox',
|
||||
required: false,
|
||||
options: '',
|
||||
placeholder: '',
|
||||
defaultValue: '',
|
||||
columnValues: {},
|
||||
}),
|
||||
|
||||
/**
|
||||
* 새 칼럼 생성 시 기본값
|
||||
*/
|
||||
getDefaultColumnFormData: (): ColumnFormData => ({
|
||||
name: '',
|
||||
key: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 기본 단위 옵션 목록
|
||||
*/
|
||||
defaultUnitOptions: [
|
||||
{ id: 'unit-1', value: 'EA', label: 'EA (개)', isActive: true },
|
||||
{ id: 'unit-2', value: 'KG', label: 'KG (킬로그램)', isActive: true },
|
||||
{ id: 'unit-3', value: 'M', label: 'M (미터)', isActive: true },
|
||||
{ id: 'unit-4', value: 'MM', label: 'MM (밀리미터)', isActive: true },
|
||||
{ id: 'unit-5', value: 'L', label: 'L (리터)', isActive: true },
|
||||
{ id: 'unit-6', value: 'SET', label: 'SET (세트)', isActive: true },
|
||||
{ id: 'unit-7', value: 'BOX', label: 'BOX (박스)', isActive: true },
|
||||
{ id: 'unit-8', value: 'ROLL', label: 'ROLL (롤)', isActive: true },
|
||||
] as MasterOption[],
|
||||
|
||||
/**
|
||||
* 기본 재질 옵션 목록
|
||||
*/
|
||||
defaultMaterialOptions: [
|
||||
{ id: 'mat-1', value: 'SUS304', label: 'SUS304 (스테인리스)', isActive: true },
|
||||
{ id: 'mat-2', value: 'SUS316', label: 'SUS316 (스테인리스)', isActive: true },
|
||||
{ id: 'mat-3', value: 'AL6061', label: 'AL6061 (알루미늄)', isActive: true },
|
||||
{ id: 'mat-4', value: 'AL5052', label: 'AL5052 (알루미늄)', isActive: true },
|
||||
{ id: 'mat-5', value: 'SS400', label: 'SS400 (일반강)', isActive: true },
|
||||
{ id: 'mat-6', value: 'S45C', label: 'S45C (탄소강)', isActive: true },
|
||||
{ id: 'mat-7', value: 'POM', label: 'POM (폴리아세탈)', isActive: true },
|
||||
{ id: 'mat-8', value: 'PEEK', label: 'PEEK (폴리에테르에테르케톤)', isActive: true },
|
||||
] as MasterOption[],
|
||||
|
||||
/**
|
||||
* 기본 표면처리 옵션 목록
|
||||
*/
|
||||
defaultSurfaceTreatmentOptions: [
|
||||
{ id: 'surf-1', value: 'NONE', label: '없음', isActive: true },
|
||||
{ id: 'surf-2', value: 'ANODIZE', label: '아노다이징', isActive: true },
|
||||
{ id: 'surf-3', value: 'PLATING', label: '도금', isActive: true },
|
||||
{ id: 'surf-4', value: 'PAINTING', label: '도장', isActive: true },
|
||||
{ id: 'surf-5', value: 'PASSIVATION', label: '부동태처리', isActive: true },
|
||||
{ id: 'surf-6', value: 'SANDBLAST', label: '샌드블라스트', isActive: true },
|
||||
{ id: 'surf-7', value: 'POLISHING', label: '폴리싱', isActive: true },
|
||||
] as MasterOption[],
|
||||
|
||||
/**
|
||||
* 속성 타입 목록
|
||||
*/
|
||||
attributeTypes: [
|
||||
{ value: 'unit', label: '단위' },
|
||||
{ value: 'material', label: '재질' },
|
||||
{ value: 'surface', label: '표면처리' },
|
||||
] as const,
|
||||
|
||||
/**
|
||||
* 칼럼 타입 목록
|
||||
*/
|
||||
columnTypes: [
|
||||
{ value: 'text', label: '텍스트' },
|
||||
{ value: 'number', label: '숫자' },
|
||||
] as const,
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Field Service
|
||||
* 필드 관련 도메인 로직 중앙화
|
||||
* - validation
|
||||
* - parsing (field_key 등)
|
||||
* - transform (폼 ↔ API)
|
||||
* - defaults
|
||||
*/
|
||||
|
||||
import type { ItemField, FieldDisplayCondition } from '@/contexts/ItemMasterContext';
|
||||
|
||||
// ===== Types =====
|
||||
|
||||
export type FieldType = 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
|
||||
export interface FieldFormData {
|
||||
name: string;
|
||||
key: string;
|
||||
inputType: FieldType;
|
||||
required: boolean;
|
||||
options: string; // 콤마 구분 문자열
|
||||
description: string;
|
||||
// 텍스트박스 컬럼
|
||||
columns?: Array<{ id: string; name: string; key: string }>;
|
||||
// 조건부 표시
|
||||
conditionEnabled?: boolean;
|
||||
conditionTargetType?: 'field' | 'section';
|
||||
conditionFields?: Array<{ fieldKey: string; expectedValue: string }>;
|
||||
conditionSections?: string[];
|
||||
}
|
||||
|
||||
export interface FieldValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
field_name?: string;
|
||||
field_key?: string;
|
||||
field_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FieldValidationError {
|
||||
valid: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface FieldValidationSuccess {
|
||||
valid: true;
|
||||
}
|
||||
|
||||
export type SingleFieldValidation = FieldValidationError | FieldValidationSuccess;
|
||||
|
||||
// ===== Service =====
|
||||
|
||||
export const fieldService = {
|
||||
// ===== Validation =====
|
||||
|
||||
/**
|
||||
* 전체 필드 폼 유효성 검사
|
||||
*/
|
||||
validate: (data: Partial<FieldFormData>): FieldValidationResult => {
|
||||
const errors: FieldValidationResult['errors'] = {};
|
||||
|
||||
// 필드명 검증
|
||||
const nameValidation = fieldService.validateFieldName(data.name || '');
|
||||
if (!nameValidation.valid) {
|
||||
errors.field_name = (nameValidation as FieldValidationError).error;
|
||||
}
|
||||
|
||||
// 필드 키 검증
|
||||
const keyValidation = fieldService.validateFieldKey(data.key || '');
|
||||
if (!keyValidation.valid) {
|
||||
errors.field_key = (keyValidation as FieldValidationError).error;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 필드명 유효성 검사
|
||||
*/
|
||||
validateFieldName: (name: string): SingleFieldValidation => {
|
||||
if (!name || !name.trim()) {
|
||||
return { valid: false, error: '항목명을 입력해주세요' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* 필드 키 유효성 검사
|
||||
* - 필수 입력
|
||||
* - 영문자로 시작
|
||||
* - 영문, 숫자, 언더스코어만 허용
|
||||
*/
|
||||
validateFieldKey: (key: string): SingleFieldValidation => {
|
||||
if (!key || !key.trim()) {
|
||||
return { valid: false, error: '필드 키를 입력해주세요' };
|
||||
}
|
||||
if (!/^[a-zA-Z]/.test(key)) {
|
||||
return { valid: false, error: '영문자로 시작해야 합니다' };
|
||||
}
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
|
||||
return { valid: false, error: '영문, 숫자, 언더스코어만 사용 가능합니다' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* 필드 키 패턴 정규식
|
||||
* UI에서 직접 사용 가능
|
||||
*/
|
||||
fieldKeyPattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
|
||||
/**
|
||||
* 필드 키가 유효한지 간단 체크 (boolean 반환)
|
||||
*/
|
||||
isFieldKeyValid: (key: string): boolean => {
|
||||
if (!key || !key.trim()) return false;
|
||||
return fieldService.fieldKeyPattern.test(key);
|
||||
},
|
||||
|
||||
// ===== Parsing =====
|
||||
|
||||
/**
|
||||
* field_key에서 사용자 입력 부분 추출
|
||||
* 형식: {ID}_{사용자입력} → 사용자입력 반환
|
||||
* 예: "123_itemCode" → "itemCode"
|
||||
*/
|
||||
extractUserInputFromFieldKey: (fieldKey: string | null | undefined): string => {
|
||||
if (!fieldKey) return '';
|
||||
|
||||
// 언더스코어가 포함된 경우 첫 번째 언더스코어 이후 부분 반환
|
||||
const underscoreIndex = fieldKey.indexOf('_');
|
||||
if (underscoreIndex !== -1) {
|
||||
return fieldKey.substring(underscoreIndex + 1);
|
||||
}
|
||||
|
||||
// 언더스코어가 없으면 전체 반환
|
||||
return fieldKey;
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 문자열을 배열로 파싱
|
||||
* "옵션1, 옵션2, 옵션3" → [{ label: "옵션1", value: "옵션1" }, ...]
|
||||
*/
|
||||
parseOptionsFromString: (optionsString: string): Array<{ label: string; value: string }> | null => {
|
||||
if (!optionsString || !optionsString.trim()) return null;
|
||||
|
||||
return optionsString
|
||||
.split(',')
|
||||
.map(opt => opt.trim())
|
||||
.filter(opt => opt.length > 0)
|
||||
.map(opt => ({ label: opt, value: opt }));
|
||||
},
|
||||
|
||||
/**
|
||||
* 옵션 배열을 문자열로 변환
|
||||
* [{ label: "옵션1", value: "옵션1" }, ...] → "옵션1, 옵션2"
|
||||
*/
|
||||
optionsToString: (options: Array<{ label: string; value: string }> | null | undefined): string => {
|
||||
if (!options || options.length === 0) return '';
|
||||
return options.map(opt => opt.value || opt.label).join(', ');
|
||||
},
|
||||
|
||||
// ===== Transform =====
|
||||
|
||||
/**
|
||||
* 폼 데이터 → API 요청 객체 변환
|
||||
*/
|
||||
toApiRequest: (
|
||||
formData: FieldFormData,
|
||||
sectionId: number,
|
||||
options?: {
|
||||
editingFieldId?: number | null;
|
||||
masterFieldId?: number | null;
|
||||
}
|
||||
): Omit<ItemField, 'created_at' | 'updated_at'> => {
|
||||
const { editingFieldId, masterFieldId } = options || {};
|
||||
|
||||
// 조건부 표시 설정
|
||||
const displayCondition: FieldDisplayCondition | undefined = formData.conditionEnabled
|
||||
? {
|
||||
targetType: formData.conditionTargetType || 'field',
|
||||
fieldConditions: formData.conditionTargetType === 'field' && formData.conditionFields?.length
|
||||
? formData.conditionFields
|
||||
: undefined,
|
||||
sectionIds: formData.conditionTargetType === 'section' && formData.conditionSections?.length
|
||||
? formData.conditionSections
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// 텍스트박스 컬럼 설정
|
||||
const hasColumns = formData.inputType === 'textbox' && formData.columns && formData.columns.length > 0;
|
||||
|
||||
return {
|
||||
id: editingFieldId || Date.now(),
|
||||
section_id: sectionId,
|
||||
master_field_id: masterFieldId || null,
|
||||
field_name: formData.name,
|
||||
field_key: formData.key,
|
||||
field_type: formData.inputType,
|
||||
order_no: 0,
|
||||
is_required: formData.required,
|
||||
placeholder: formData.description || null,
|
||||
default_value: null,
|
||||
display_condition: displayCondition as Record<string, any> | null || null,
|
||||
validation_rules: null,
|
||||
options: formData.inputType === 'dropdown'
|
||||
? fieldService.parseOptionsFromString(formData.options)
|
||||
: null,
|
||||
properties: hasColumns
|
||||
? {
|
||||
multiColumn: true,
|
||||
columnCount: formData.columns!.length,
|
||||
columnNames: formData.columns!.map(c => c.name),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* ItemField → 폼 데이터 변환 (수정 시 폼에 채우기 위함)
|
||||
*/
|
||||
toFormData: (field: ItemField): FieldFormData => {
|
||||
return {
|
||||
name: field.field_name,
|
||||
key: fieldService.extractUserInputFromFieldKey(field.field_key),
|
||||
inputType: field.field_type,
|
||||
required: field.is_required || (field.properties as any)?.required || false,
|
||||
options: fieldService.optionsToString(field.options),
|
||||
description: field.placeholder || '',
|
||||
// 조건부 표시
|
||||
conditionEnabled: !!field.display_condition,
|
||||
conditionTargetType: (field.display_condition as any)?.targetType || 'field',
|
||||
conditionFields: (field.display_condition as any)?.fieldConditions || [],
|
||||
conditionSections: (field.display_condition as any)?.sectionIds || [],
|
||||
// 텍스트박스 컬럼 (properties에서 복원 - 필요시 구현)
|
||||
columns: [],
|
||||
};
|
||||
},
|
||||
|
||||
// ===== Defaults =====
|
||||
|
||||
/**
|
||||
* 새 필드 생성 시 기본값
|
||||
*/
|
||||
getDefaultFormData: (): FieldFormData => ({
|
||||
name: '',
|
||||
key: '',
|
||||
inputType: 'textbox',
|
||||
required: false,
|
||||
options: '',
|
||||
description: '',
|
||||
columns: [],
|
||||
conditionEnabled: false,
|
||||
conditionTargetType: 'field',
|
||||
conditionFields: [],
|
||||
conditionSections: [],
|
||||
}),
|
||||
|
||||
/**
|
||||
* 지원하는 필드 타입 목록
|
||||
*/
|
||||
fieldTypes: [
|
||||
{ value: 'textbox', label: '텍스트박스' },
|
||||
{ value: 'number', label: '숫자' },
|
||||
{ value: 'dropdown', label: '드롭다운' },
|
||||
{ value: 'checkbox', label: '체크박스' },
|
||||
{ value: 'date', label: '날짜' },
|
||||
{ value: 'textarea', label: '텍스트영역' },
|
||||
] as const,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
// 품목기준관리 서비스 레이어
|
||||
// 도메인 로직 중앙화 - validation, parsing, transform
|
||||
|
||||
export { fieldService } from './fieldService';
|
||||
export type { FieldValidationResult, FieldFormData } from './fieldService';
|
||||
|
||||
export { masterFieldService } from './masterFieldService';
|
||||
export type { MasterFieldValidationResult, MasterFieldFormData } from './masterFieldService';
|
||||
|
||||
export { sectionService } from './sectionService';
|
||||
export type { SectionValidationResult, SectionFormData, SectionType, SectionInputType } from './sectionService';
|
||||
|
||||
export { pageService } from './pageService';
|
||||
export type { PageValidationResult, PageFormData, ItemType } from './pageService';
|
||||
|
||||
export { templateService } from './templateService';
|
||||
export type {
|
||||
TemplateValidationResult,
|
||||
TemplateFieldValidationResult,
|
||||
SectionTemplateFormData,
|
||||
TemplateFieldFormData,
|
||||
TemplateType,
|
||||
} from './templateService';
|
||||
|
||||
export { attributeService } from './attributeService';
|
||||
export type {
|
||||
OptionValidationResult,
|
||||
ColumnValidationResult,
|
||||
OptionFormData,
|
||||
ColumnFormData,
|
||||
AttributeType,
|
||||
ColumnType,
|
||||
} from './attributeService';
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Master Field Service
|
||||
* 마스터 필드(항목탭) 관련 도메인 로직 중앙화
|
||||
* - validation (fieldService 재사용)
|
||||
* - parsing
|
||||
* - transform (폼 ↔ API)
|
||||
* - defaults
|
||||
*/
|
||||
|
||||
import type { ItemMasterField } from '@/contexts/ItemMasterContext';
|
||||
import { fieldService, type SingleFieldValidation } from './fieldService';
|
||||
|
||||
// ===== Types =====
|
||||
|
||||
export type MasterFieldType = 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
export type AttributeType = 'custom' | 'unit' | 'material' | 'surface';
|
||||
|
||||
export interface MasterFieldFormData {
|
||||
name: string;
|
||||
key: string;
|
||||
inputType: MasterFieldType;
|
||||
required: boolean;
|
||||
category: string;
|
||||
description: string;
|
||||
options: string; // 콤마 구분 문자열
|
||||
attributeType: AttributeType;
|
||||
multiColumn: boolean;
|
||||
columnCount: number;
|
||||
columnNames: string[];
|
||||
}
|
||||
|
||||
export interface MasterFieldValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
field_name?: string;
|
||||
field_key?: string;
|
||||
field_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Service =====
|
||||
|
||||
export const masterFieldService = {
|
||||
// ===== Validation (fieldService 재사용) =====
|
||||
|
||||
/**
|
||||
* 전체 마스터 필드 폼 유효성 검사
|
||||
*/
|
||||
validate: (data: Partial<MasterFieldFormData>): MasterFieldValidationResult => {
|
||||
const errors: MasterFieldValidationResult['errors'] = {};
|
||||
|
||||
// 필드명 검증 (fieldService 재사용)
|
||||
const nameValidation = fieldService.validateFieldName(data.name || '');
|
||||
if (!nameValidation.valid) {
|
||||
errors.field_name = (nameValidation as { valid: false; error: string }).error;
|
||||
}
|
||||
|
||||
// 필드 키 검증 (fieldService 재사용)
|
||||
const keyValidation = fieldService.validateFieldKey(data.key || '');
|
||||
if (!keyValidation.valid) {
|
||||
errors.field_key = (keyValidation as { valid: false; error: string }).error;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 필드명 유효성 검사 (fieldService 위임)
|
||||
*/
|
||||
validateFieldName: fieldService.validateFieldName,
|
||||
|
||||
/**
|
||||
* 필드 키 유효성 검사 (fieldService 위임)
|
||||
*/
|
||||
validateFieldKey: fieldService.validateFieldKey,
|
||||
|
||||
/**
|
||||
* 필드 키 패턴 정규식 (fieldService 재사용)
|
||||
*/
|
||||
fieldKeyPattern: fieldService.fieldKeyPattern,
|
||||
|
||||
/**
|
||||
* 필드 키가 유효한지 간단 체크 (fieldService 위임)
|
||||
*/
|
||||
isFieldKeyValid: fieldService.isFieldKeyValid,
|
||||
|
||||
// ===== Parsing =====
|
||||
|
||||
/**
|
||||
* field_key에서 사용자 입력 부분 추출 (fieldService 위임)
|
||||
* 형식: {ID}_{사용자입력} → 사용자입력 반환
|
||||
*/
|
||||
extractUserInputFromFieldKey: fieldService.extractUserInputFromFieldKey,
|
||||
|
||||
/**
|
||||
* 옵션 문자열을 배열로 파싱 (fieldService 위임)
|
||||
*/
|
||||
parseOptionsFromString: fieldService.parseOptionsFromString,
|
||||
|
||||
/**
|
||||
* 옵션 배열을 문자열로 변환 (fieldService 위임)
|
||||
*/
|
||||
optionsToString: fieldService.optionsToString,
|
||||
|
||||
// ===== Transform =====
|
||||
|
||||
/**
|
||||
* 폼 데이터 → API 요청 객체 변환
|
||||
*/
|
||||
toApiRequest: (
|
||||
formData: MasterFieldFormData
|
||||
): Omit<ItemMasterField, 'id' | 'tenant_id' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'> => {
|
||||
const supportsMultiColumn = formData.inputType === 'textbox' || formData.inputType === 'textarea';
|
||||
|
||||
return {
|
||||
field_name: formData.name,
|
||||
field_key: formData.key,
|
||||
field_type: formData.inputType,
|
||||
category: formData.category || null,
|
||||
description: formData.description || null,
|
||||
is_common: false,
|
||||
default_value: null,
|
||||
options: formData.inputType === 'dropdown'
|
||||
? fieldService.parseOptionsFromString(formData.options)
|
||||
: null,
|
||||
validation_rules: null,
|
||||
properties: {
|
||||
required: formData.required,
|
||||
attributeType: formData.inputType === 'dropdown' ? formData.attributeType : undefined,
|
||||
multiColumn: supportsMultiColumn ? formData.multiColumn : undefined,
|
||||
columnCount: supportsMultiColumn && formData.multiColumn ? formData.columnCount : undefined,
|
||||
columnNames: supportsMultiColumn && formData.multiColumn ? formData.columnNames : undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* ItemMasterField → 폼 데이터 변환 (수정 시 폼에 채우기 위함)
|
||||
*/
|
||||
toFormData: (field: ItemMasterField): MasterFieldFormData => {
|
||||
const properties = field.properties as Record<string, any> | null;
|
||||
|
||||
return {
|
||||
name: field.field_name,
|
||||
key: masterFieldService.extractUserInputFromFieldKey(field.field_key),
|
||||
inputType: field.field_type || 'textbox',
|
||||
required: properties?.required || false,
|
||||
category: field.category || '공통',
|
||||
description: field.description || '',
|
||||
options: masterFieldService.optionsToString(field.options),
|
||||
attributeType: properties?.attributeType || 'custom',
|
||||
multiColumn: properties?.multiColumn || false,
|
||||
columnCount: properties?.columnCount || 2,
|
||||
columnNames: properties?.columnNames || ['컬럼1', '컬럼2'],
|
||||
};
|
||||
},
|
||||
|
||||
// ===== Defaults =====
|
||||
|
||||
/**
|
||||
* 새 마스터 필드 생성 시 기본값
|
||||
*/
|
||||
getDefaultFormData: (): MasterFieldFormData => ({
|
||||
name: '',
|
||||
key: '',
|
||||
inputType: 'textbox',
|
||||
required: false,
|
||||
category: '공통',
|
||||
description: '',
|
||||
options: '',
|
||||
attributeType: 'custom',
|
||||
multiColumn: false,
|
||||
columnCount: 2,
|
||||
columnNames: ['컬럼1', '컬럼2'],
|
||||
}),
|
||||
|
||||
/**
|
||||
* 지원하는 필드 타입 목록 (fieldService 재사용)
|
||||
*/
|
||||
fieldTypes: fieldService.fieldTypes,
|
||||
|
||||
/**
|
||||
* 지원하는 속성 타입 목록
|
||||
*/
|
||||
attributeTypes: [
|
||||
{ value: 'custom', label: '직접 입력' },
|
||||
{ value: 'unit', label: '단위' },
|
||||
{ value: 'material', label: '재질' },
|
||||
{ value: 'surface', label: '표면처리' },
|
||||
] as const,
|
||||
|
||||
/**
|
||||
* 다중 컬럼 지원 여부 확인
|
||||
*/
|
||||
supportsMultiColumn: (inputType: MasterFieldType): boolean => {
|
||||
return inputType === 'textbox' || inputType === 'textarea';
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Section Service
|
||||
* 섹션 관련 도메인 로직 중앙화
|
||||
* - validation
|
||||
* - parsing
|
||||
* - transform (폼 ↔ API)
|
||||
* - defaults
|
||||
*/
|
||||
|
||||
import type { ItemSection, ItemPage } from '@/contexts/ItemMasterContext';
|
||||
|
||||
// ===== Types =====
|
||||
|
||||
export type SectionType = 'BASIC' | 'BOM' | 'CUSTOM';
|
||||
export type SectionInputType = 'fields' | 'bom';
|
||||
|
||||
export interface SectionFormData {
|
||||
title: string;
|
||||
description: string;
|
||||
sectionType: SectionInputType;
|
||||
inputMode: 'custom' | 'template';
|
||||
templateId: number | null;
|
||||
}
|
||||
|
||||
export interface SectionValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
title?: string;
|
||||
section_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Service =====
|
||||
|
||||
export const sectionService = {
|
||||
// ===== Validation =====
|
||||
|
||||
/**
|
||||
* 전체 섹션 폼 유효성 검사
|
||||
*/
|
||||
validate: (data: Partial<SectionFormData>): SectionValidationResult => {
|
||||
const errors: SectionValidationResult['errors'] = {};
|
||||
|
||||
const titleValidation = sectionService.validateTitle(data.title || '');
|
||||
if (!titleValidation.valid) {
|
||||
errors.title = titleValidation.error;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 섹션 제목 유효성 검사
|
||||
*/
|
||||
validateTitle: (title: string): { valid: boolean; error?: string } => {
|
||||
if (!title || !title.trim()) {
|
||||
return { valid: false, error: '섹션 제목을 입력해주세요' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
/**
|
||||
* 섹션 타입 유효성 검사
|
||||
*/
|
||||
validateSectionType: (type: string): { valid: boolean; error?: string } => {
|
||||
const validTypes = ['BASIC', 'BOM', 'CUSTOM'];
|
||||
if (!validTypes.includes(type)) {
|
||||
return { valid: false, error: '유효하지 않은 섹션 타입입니다' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
// ===== Transform =====
|
||||
|
||||
/**
|
||||
* UI 섹션 타입을 API 섹션 타입으로 변환
|
||||
*/
|
||||
toApiSectionType: (inputType: SectionInputType): SectionType => {
|
||||
return inputType === 'bom' ? 'BOM' : 'BASIC';
|
||||
},
|
||||
|
||||
/**
|
||||
* API 섹션 타입을 UI 섹션 타입으로 변환
|
||||
*/
|
||||
toUiSectionType: (sectionType: SectionType): SectionInputType => {
|
||||
return sectionType === 'BOM' ? 'bom' : 'fields';
|
||||
},
|
||||
|
||||
/**
|
||||
* 폼 데이터 → API 요청 객체 변환
|
||||
*/
|
||||
toApiRequest: (
|
||||
formData: SectionFormData,
|
||||
pageId: number,
|
||||
orderNo: number
|
||||
): Omit<ItemSection, 'id' | 'created_at' | 'updated_at'> => {
|
||||
const sectionType = sectionService.toApiSectionType(formData.sectionType);
|
||||
|
||||
return {
|
||||
page_id: pageId,
|
||||
title: formData.title,
|
||||
section_type: sectionType,
|
||||
description: formData.description || undefined,
|
||||
order_no: orderNo,
|
||||
is_template: false,
|
||||
is_default: false,
|
||||
is_collapsible: true,
|
||||
is_default_open: true,
|
||||
fields: [],
|
||||
bom_items: sectionType === 'BOM' ? [] : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* ItemSection → 폼 데이터 변환
|
||||
*/
|
||||
toFormData: (section: ItemSection): SectionFormData => {
|
||||
return {
|
||||
title: section.title,
|
||||
description: section.description || '',
|
||||
sectionType: sectionService.toUiSectionType(section.section_type),
|
||||
inputMode: 'custom',
|
||||
templateId: null,
|
||||
};
|
||||
},
|
||||
|
||||
// ===== Utilities =====
|
||||
|
||||
/**
|
||||
* 새 섹션 생성 시 기본값
|
||||
*/
|
||||
getDefaultFormData: (): SectionFormData => ({
|
||||
title: '',
|
||||
description: '',
|
||||
sectionType: 'fields',
|
||||
inputMode: 'custom',
|
||||
templateId: null,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 섹션이 이미 페이지에 연결되어 있는지 확인
|
||||
*/
|
||||
isLinkedToPage: (sectionId: number, page: ItemPage): boolean => {
|
||||
return page.sections.some(s => s.id === sectionId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 섹션 타입 옵션 목록
|
||||
*/
|
||||
sectionTypes: [
|
||||
{ value: 'fields', label: '일반 섹션', description: '필드 항목 관리' },
|
||||
{ value: 'bom', label: '모듈 섹션 (BOM)', description: '자재명세서 관리' },
|
||||
] as const,
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Template Service
|
||||
* 섹션 템플릿 및 템플릿 필드 관련 도메인 로직 중앙화
|
||||
* - validation (fieldService 재사용)
|
||||
* - parsing (fieldService 재사용)
|
||||
* - transform (폼 ↔ API)
|
||||
* - defaults
|
||||
*/
|
||||
|
||||
import type { SectionTemplate, TemplateField, ItemSection } from '@/contexts/ItemMasterContext';
|
||||
import { fieldService } from './fieldService';
|
||||
|
||||
// ===== Types =====
|
||||
|
||||
export type TemplateType = 'fields' | 'bom';
|
||||
|
||||
export interface SectionTemplateFormData {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string[];
|
||||
templateType: TemplateType;
|
||||
}
|
||||
|
||||
export interface TemplateFieldFormData {
|
||||
name: string;
|
||||
key: string;
|
||||
inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
||||
required: boolean;
|
||||
options: string;
|
||||
description: string;
|
||||
multiColumn: boolean;
|
||||
columnCount: number;
|
||||
columnNames: string[];
|
||||
inputMode: 'custom' | 'master';
|
||||
selectedMasterFieldId: string;
|
||||
}
|
||||
|
||||
export interface TemplateValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
title?: string;
|
||||
template_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TemplateFieldValidationResult {
|
||||
valid: boolean;
|
||||
errors: {
|
||||
field_name?: string;
|
||||
field_key?: string;
|
||||
field_type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Service =====
|
||||
|
||||
export const templateService = {
|
||||
// ===== Section Template Validation =====
|
||||
|
||||
/**
|
||||
* 섹션 템플릿 폼 유효성 검사
|
||||
*/
|
||||
validate: (data: Partial<SectionTemplateFormData>): TemplateValidationResult => {
|
||||
const errors: TemplateValidationResult['errors'] = {};
|
||||
|
||||
if (!data.title || !data.title.trim()) {
|
||||
errors.title = '섹션 제목을 입력해주세요';
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 섹션 템플릿 제목 유효성 검사
|
||||
*/
|
||||
validateTitle: (title: string): { valid: boolean; error?: string } => {
|
||||
if (!title || !title.trim()) {
|
||||
return { valid: false, error: '섹션 제목을 입력해주세요' };
|
||||
}
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
// ===== Template Field Validation (fieldService 재사용) =====
|
||||
|
||||
/**
|
||||
* 템플릿 필드 폼 유효성 검사
|
||||
*/
|
||||
validateField: (data: Partial<TemplateFieldFormData>): TemplateFieldValidationResult => {
|
||||
const errors: TemplateFieldValidationResult['errors'] = {};
|
||||
|
||||
const nameValidation = fieldService.validateFieldName(data.name || '');
|
||||
if (!nameValidation.valid) {
|
||||
errors.field_name = (nameValidation as { valid: false; error: string }).error;
|
||||
}
|
||||
|
||||
const keyValidation = fieldService.validateFieldKey(data.key || '');
|
||||
if (!keyValidation.valid) {
|
||||
errors.field_key = (keyValidation as { valid: false; error: string }).error;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 필드명 유효성 검사 (fieldService 위임)
|
||||
*/
|
||||
validateFieldName: fieldService.validateFieldName,
|
||||
|
||||
/**
|
||||
* 필드 키 유효성 검사 (fieldService 위임)
|
||||
*/
|
||||
validateFieldKey: fieldService.validateFieldKey,
|
||||
|
||||
/**
|
||||
* 필드 키 패턴 정규식 (fieldService 재사용)
|
||||
*/
|
||||
fieldKeyPattern: fieldService.fieldKeyPattern,
|
||||
|
||||
// ===== Parsing (fieldService 재사용) =====
|
||||
|
||||
/**
|
||||
* field_key에서 사용자 입력 부분 추출 (fieldService 위임)
|
||||
*/
|
||||
extractUserInputFromFieldKey: fieldService.extractUserInputFromFieldKey,
|
||||
|
||||
/**
|
||||
* 옵션 문자열을 배열로 파싱 (fieldService 위임)
|
||||
*/
|
||||
parseOptionsFromString: fieldService.parseOptionsFromString,
|
||||
|
||||
/**
|
||||
* 옵션 배열을 문자열로 변환 (fieldService 위임)
|
||||
*/
|
||||
optionsToString: fieldService.optionsToString,
|
||||
|
||||
// ===== Transform =====
|
||||
|
||||
/**
|
||||
* 섹션 템플릿 폼 데이터 → API 섹션 타입 변환
|
||||
*/
|
||||
toApiSectionType: (templateType: TemplateType): 'BASIC' | 'BOM' | 'CUSTOM' => {
|
||||
return templateType === 'bom' ? 'BOM' : 'BASIC';
|
||||
},
|
||||
|
||||
/**
|
||||
* API 섹션 타입 → UI 템플릿 타입 변환
|
||||
*/
|
||||
toUiTemplateType: (sectionType: 'BASIC' | 'BOM' | 'CUSTOM'): TemplateType => {
|
||||
return sectionType === 'BOM' ? 'bom' : 'fields';
|
||||
},
|
||||
|
||||
/**
|
||||
* SectionTemplate → 폼 데이터 변환
|
||||
*/
|
||||
toFormData: (template: SectionTemplate): SectionTemplateFormData => {
|
||||
return {
|
||||
title: template.template_name,
|
||||
description: template.description || '',
|
||||
category: template.category || [],
|
||||
templateType: templateService.toUiTemplateType(template.section_type),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 폼 데이터 → 독립 섹션 생성 요청 객체 변환
|
||||
*/
|
||||
toIndependentSectionRequest: (formData: SectionTemplateFormData) => {
|
||||
return {
|
||||
title: formData.title,
|
||||
type: formData.templateType as 'fields' | 'bom',
|
||||
description: formData.description || undefined,
|
||||
is_template: true,
|
||||
is_default: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 폼 데이터 → 섹션 업데이트 요청 객체 변환
|
||||
*/
|
||||
toUpdateRequest: (formData: SectionTemplateFormData): Partial<ItemSection> => {
|
||||
return {
|
||||
title: formData.title,
|
||||
description: formData.description || undefined,
|
||||
section_type: templateService.toApiSectionType(formData.templateType),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* TemplateField → 폼 데이터 변환
|
||||
*/
|
||||
fieldToFormData: (field: TemplateField): TemplateFieldFormData => {
|
||||
return {
|
||||
name: field.name,
|
||||
key: templateService.extractUserInputFromFieldKey(field.fieldKey),
|
||||
inputType: field.property.inputType,
|
||||
required: field.property.required,
|
||||
options: field.property.options?.join(', ') || '',
|
||||
description: field.description || '',
|
||||
multiColumn: field.property.multiColumn || false,
|
||||
columnCount: field.property.columnCount || 2,
|
||||
columnNames: field.property.columnNames || ['컬럼1', '컬럼2'],
|
||||
inputMode: 'custom',
|
||||
selectedMasterFieldId: '',
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 폼 데이터 → 필드 업데이트 요청 객체 변환
|
||||
*/
|
||||
fieldToUpdateRequest: (formData: TemplateFieldFormData) => {
|
||||
const supportsMultiColumn = formData.inputType === 'textbox' || formData.inputType === 'textarea';
|
||||
|
||||
return {
|
||||
field_name: formData.name,
|
||||
field_key: formData.key,
|
||||
field_type: formData.inputType,
|
||||
is_required: formData.required,
|
||||
placeholder: formData.description || null,
|
||||
options: formData.inputType === 'dropdown' && formData.options.trim()
|
||||
? formData.options.split(',').map(o => ({ label: o.trim(), value: o.trim() }))
|
||||
: null,
|
||||
properties: {
|
||||
inputType: formData.inputType,
|
||||
required: formData.required,
|
||||
multiColumn: supportsMultiColumn ? formData.multiColumn : undefined,
|
||||
columnCount: supportsMultiColumn && formData.multiColumn ? formData.columnCount : undefined,
|
||||
columnNames: supportsMultiColumn && formData.multiColumn ? formData.columnNames : undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// ===== Defaults =====
|
||||
|
||||
/**
|
||||
* 새 섹션 템플릿 생성 시 기본값
|
||||
*/
|
||||
getDefaultFormData: (): SectionTemplateFormData => ({
|
||||
title: '',
|
||||
description: '',
|
||||
category: [],
|
||||
templateType: 'fields',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 새 템플릿 필드 생성 시 기본값
|
||||
*/
|
||||
getDefaultFieldFormData: (): TemplateFieldFormData => ({
|
||||
name: '',
|
||||
key: '',
|
||||
inputType: 'textbox',
|
||||
required: false,
|
||||
options: '',
|
||||
description: '',
|
||||
multiColumn: false,
|
||||
columnCount: 2,
|
||||
columnNames: ['컬럼1', '컬럼2'],
|
||||
inputMode: 'custom',
|
||||
selectedMasterFieldId: '',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 지원하는 필드 타입 목록 (fieldService 재사용)
|
||||
*/
|
||||
fieldTypes: fieldService.fieldTypes,
|
||||
|
||||
/**
|
||||
* 섹션 타입 옵션 목록
|
||||
*/
|
||||
sectionTypes: [
|
||||
{ value: 'fields', label: '일반 섹션' },
|
||||
{ value: 'bom', label: '모듈 섹션 (BOM)' },
|
||||
] as const,
|
||||
};
|
||||
Reference in New Issue
Block a user