/** * Field Service * 필드 관련 도메인 로직 중앙화 * - validation * - parsing (field_key 등) * - transform (폼 ↔ API) * - defaults */ import type { ItemField, FieldDisplayCondition } from '@/contexts/ItemMasterContext'; import type { ItemFieldType } from '@/types/item-master-api'; // ===== Types ===== export type FieldType = ItemFieldType; 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): 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 }; }, /** * 필드 키 유효성 검사 * - 필수 입력 * - 영문, 숫자, 언더스코어만 허용 * 2025-12-16: 숫자로 시작하는 키도 허용 (예: 105_state) */ validateFieldKey: (key: string): SingleFieldValidation => { if (!key || !key.trim()) { return { valid: false, error: '필드 키를 입력해주세요' }; } if (!/^[a-zA-Z0-9_]+$/.test(key)) { return { valid: false, error: '영문, 숫자, 언더스코어만 사용 가능합니다' }; } return { valid: true }; }, /** * 필드 키 패턴 정규식 * UI에서 직접 사용 가능 * 2025-12-16: 숫자로 시작하는 키도 허용 */ fieldKeyPattern: /^[a-zA-Z0-9_]+$/, /** * 필드 키가 유효한지 간단 체크 (boolean 반환) */ isFieldKeyValid: (key: string): boolean => { if (!key || !key.trim()) return false; return fieldService.fieldKeyPattern.test(key); }, // ===== Parsing ===== /** * field_key 반환 (전체 키 그대로 반환) * 2025-12-16: 전체 field_key 표시로 변경 (예: "105_state" 그대로 표시) */ extractUserInputFromFieldKey: (fieldKey: string | null | undefined): string => { if (!fieldKey) return ''; 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 => { 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 | 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, };