Files
sam-react-prod/src/components/items/ItemMasterDataManagement/services/fieldService.ts
byeongcheolryu 0552b02ba9 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>
2025-12-01 14:23:57 +09:00

276 lines
8.0 KiB
TypeScript

/**
* 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,
};