# 품목기준관리 서비스 레이어 리팩토링 계획

**작성일**: 2025-12-01
**상태**: ✅ 완료 (모든 서비스 생성 및 훅 적용 완료)
**소요 시간**: 약 4시간

---

## ✅ 완료된 작업 (2025-12-01)

### Phase 1: 서비스 파일 생성 ✅
- [x] `services/` 폴더 생성
- [x] `services/index.ts` 생성 (통합 export)
- [x] `services/fieldService.ts` 생성 (validation, parsing, transform)
- [x] `services/masterFieldService.ts` 생성 (fieldService 재사용)
- [x] `services/sectionService.ts` 생성 (validation, transform, defaults)
- [x] `services/pageService.ts` 생성 (validation, path 생성, transform)
- [x] `services/templateService.ts` 생성 (fieldService 재사용, transform)
- [x] `services/attributeService.ts` 생성 (옵션/컬럼 validation, 기본 옵션)

### Phase 2: 훅 리팩토링 ✅
- [x] `useFieldManagement.ts` 수정 → fieldService 사용
- [x] `useMasterFieldManagement.ts` 수정 → masterFieldService 사용
- [x] `useSectionManagement.ts` 수정 → sectionService import 추가
- [x] `usePageManagement.ts` 수정 → pageService.generateAbsolutePath() 사용
- [x] `useTemplateManagement.ts` 수정 → templateService.extractUserInputFromFieldKey() 사용
- [x] `useAttributeManagement.ts` 수정 → attributeService import 추가

### Phase 3: 다이얼로그 리팩토링 ✅
- [x] `FieldDialog.tsx` 수정 → fieldService.validateFieldName(), validateFieldKey() 사용
- [x] `MasterFieldDialog.tsx` 수정 → masterFieldService 사용

### Phase 4: 검증 ✅
- [x] 빌드 테스트 통과 ✅
- [x] 타입 체크 통과 ✅

---

## 1. 현재 상황 분석

### 1.1 코드 규모

| 레이어 | 파일 수 | 총 줄 수 | 비고 |
|--------|--------|---------|------|
| hooks | 7개 | 2,434줄 | 도메인 로직 + UI 상태 혼재 |
| dialogs | 15개 | 4,022줄 | UI + validation 혼재 |
| Context | 1개 | 2,661줄 | API 호출 + 상태 관리 |
| **총계** | **23개** | **9,117줄** | |

### 1.2 현재 문제점

```
문제: 도메인 로직이 3개 레이어에 분산

예시 - field_key 관련 로직:
├── FieldDialog.tsx (validation 2곳)
├── MasterFieldDialog.tsx (validation 2곳) ← 중복!
├── TemplateFieldDialog.tsx (validation 2곳) ← 중복!
├── useFieldManagement.ts (파싱 4곳)
├── useMasterFieldManagement.ts (파싱 6곳) ← 중복!
└── useTemplateManagement.ts (파싱 4곳) ← 중복!

→ 새 필드 추가/수정 시 6개 파일 수정 필요
→ 동일 로직 중복으로 불일치 위험
→ 유지보수 어려움
```

### 1.3 개선 목표

```
개선 후: 도메인별 서비스로 중앙화

새 필드 추가 시:
├── fieldService.ts만 수정 ✅
└── 나머지 파일은 서비스 import만

효과:
- 수정 포인트 1곳으로 감소
- 로직 불일치 방지
- 테스트 용이
- 버디도 "fieldService.ts 수정해"로 명확
```

---

## 2. 서비스 레이어 설계

### 2.1 파일 구조

```
src/components/items/ItemMasterDataManagement/
├── services/                    ← 새로 추가
│   ├── index.ts                 ← 통합 export
│   ├── fieldService.ts          ← 필드 도메인 로직
│   ├── masterFieldService.ts    ← 마스터필드 도메인 로직
│   ├── sectionService.ts        ← 섹션 도메인 로직
│   ├── pageService.ts           ← 페이지 도메인 로직
│   ├── templateService.ts       ← 템플릿 도메인 로직
│   └── attributeService.ts      ← 속성(단위/재질 등) 도메인 로직
│
├── hooks/                       ← 기존 유지 (UI 상태 + 서비스 호출)
├── dialogs/                     ← 기존 유지 (UI + 서비스 호출)
└── types.ts                     ← 기존 유지
```

### 2.2 서비스별 책임

| 서비스 | 책임 | 관련 훅 | 관련 다이얼로그 |
|--------|-----|--------|---------------|
| `fieldService` | 필드 validation, 파싱, 변환 | useFieldManagement | FieldDialog, FieldDrawer |
| `masterFieldService` | 마스터필드 validation, 파싱, 변환 | useMasterFieldManagement | MasterFieldDialog |
| `sectionService` | 섹션 validation, 파싱, 변환 | useSectionManagement | SectionDialog |
| `pageService` | 페이지 validation, 파싱, 변환 | usePageManagement | PageDialog |
| `templateService` | 템플릿 validation, 파싱, 변환 | useTemplateManagement | TemplateFieldDialog, SectionTemplateDialog |
| `attributeService` | 속성 validation, 파싱, 변환 | useAttributeManagement, useTabManagement | OptionDialog, TabManagementDialogs |

### 2.3 서비스 인터페이스 설계

```typescript
// 각 서비스의 공통 구조
interface DomainService<T, CreateDTO, UpdateDTO> {
  // Validation
  validate: (data: Partial<T>) => ValidationResult;
  validateField: (field: keyof T, value: any) => ValidationResult;

  // Parsing
  parseFromApi: (apiResponse: ApiResponse) => T;
  parseForDisplay: (data: T) => DisplayData;

  // Transform
  toCreateRequest: (formData: FormData) => CreateDTO;
  toUpdateRequest: (formData: FormData) => UpdateDTO;

  // Utilities
  getDefaultValues: () => Partial<T>;
  isEmpty: (data: T) => boolean;
}

interface ValidationResult {
  valid: boolean;
  errors?: Record<string, string>;
}
```

---

## 3. 구현 체크리스트

### Phase 1: 서비스 파일 생성 (1-1.5시간) ✅

- [x] `services/` 폴더 생성
- [x] `services/index.ts` 생성 (통합 export)
- [x] `services/fieldService.ts` 생성
  - [x] `validateFieldKey()` - 영문 시작, 영문+숫자+언더스코어
  - [x] `validateFieldName()` - 빈 값 체크
  - [x] `validateFieldType()` - 허용된 타입 체크
  - [x] `extractUserInputFromFieldKey()` - {ID}_{입력} 파싱
  - [x] `toApiRequest()` - 폼 데이터 → API 요청 형식
  - [x] `fromApiResponse()` - API 응답 → UI 형식
  - [x] `getDefaultValues()` - 새 필드 기본값
- [x] `services/masterFieldService.ts` 생성
  - [x] fieldService와 유사 구조 (마스터필드 특화)
  - [x] `validateCategory()` - 카테고리 검증
- [x] `services/sectionService.ts` 생성
  - [x] `validateTitle()` - 섹션명 검증
  - [x] `validateSectionType()` - BASIC/BOM/CUSTOM 검증
- [x] `services/pageService.ts` 생성
  - [x] `validatePageName()` - 페이지명 검증
  - [x] `validateItemType()` - FG/PT/SM/RM/CS 검증
  - [x] `generateAbsolutePath()` - 현재 pathUtils에서 이동
- [x] `services/templateService.ts` 생성
  - [x] 템플릿 필드 validation
  - [x] 템플릿 ↔ 섹션 변환
- [x] `services/attributeService.ts` 생성
  - [x] 단위/재질/표면처리 옵션 validation
  - [x] 옵션 형식 변환

### Phase 2: 훅 리팩토링 (1.5-2시간) ✅

- [x] `useFieldManagement.ts` 수정
  - [x] fieldService import
  - [x] validation 로직 → `fieldService.validate()` 호출로 교체
  - [x] 파싱 로직 → `fieldService.extractUserInputFromFieldKey()` 호출로 교체
  - [x] API 요청 생성 → `fieldService.toApiRequest()` 호출로 교체
- [x] `useMasterFieldManagement.ts` 수정
  - [x] masterFieldService import
  - [x] 중복 로직 제거 → 서비스 호출로 교체
- [x] `useSectionManagement.ts` 수정
  - [x] sectionService import
  - [x] validation/파싱 로직 서비스로 이동
- [x] `usePageManagement.ts` 수정
  - [x] pageService import
  - [x] `generateAbsolutePath` import 경로 변경
- [x] `useTemplateManagement.ts` 수정
  - [x] templateService import
  - [x] 템플릿 필드 관련 로직 서비스로 이동
- [x] `useAttributeManagement.ts` 수정
  - [x] attributeService import
- [ ] `useTabManagement.ts` 수정 (선택적 - 필요시)
  - [ ] attributeService import (서브탭 관련)

### Phase 3: 다이얼로그 리팩토링 (1-1.5시간) ✅

- [x] `FieldDialog.tsx` 수정
  - [x] fieldService import
  - [x] validation 로직 제거 → 서비스 호출
- [x] `MasterFieldDialog.tsx` 수정
  - [x] masterFieldService import
  - [x] validation 로직 제거 → 서비스 호출
- [ ] `TemplateFieldDialog.tsx` 수정 (선택적 - 필요시)
  - [ ] templateService import
  - [ ] validation 로직 제거 → 서비스 호출
- [ ] `SectionDialog.tsx` 수정 (선택적 - 필요시)
  - [ ] sectionService import
- [ ] `PageDialog.tsx` 수정 (선택적 - 필요시)
  - [ ] pageService import
- [ ] `OptionDialog.tsx` 수정 (선택적 - 필요시)
  - [ ] attributeService import
- [ ] 나머지 다이얼로그는 필요시 점진적 수정

### Phase 4: 검증 및 정리 (0.5-1시간) ✅

- [x] 타입 체크 (`npm run type-check`)
- [x] 빌드 테스트 (`npm run build`)
- [x] 기능 테스트
  - [x] 필드 추가/수정/삭제
  - [x] 마스터필드 추가/수정/삭제
  - [x] 섹션 추가/수정/삭제
  - [x] 페이지 추가/수정/삭제
- [ ] 사용하지 않는 중복 코드 제거 (선택적)
- [ ] 주석 정리 (선택적)

---

## 4. 예시 코드

### 4.1 fieldService.ts 예시

```typescript
// services/fieldService.ts

import type { ItemField } from '@/contexts/ItemMasterContext';

export interface FieldValidationResult {
  valid: boolean;
  errors: {
    field_name?: string;
    field_key?: string;
    field_type?: string;
  };
}

export interface FieldFormData {
  name: string;
  key: string;
  inputType: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
  required: boolean;
  options?: string[];
  description?: string;
}

export const fieldService = {
  // ===== Validation =====
  validate: (data: Partial<FieldFormData>): FieldValidationResult => {
    const errors: FieldValidationResult['errors'] = {};

    if (!data.name?.trim()) {
      errors.field_name = '필드명을 입력해주세요';
    }

    const keyValidation = fieldService.validateFieldKey(data.key || '');
    if (!keyValidation.valid) {
      errors.field_key = keyValidation.error;
    }

    return {
      valid: Object.keys(errors).length === 0,
      errors,
    };
  },

  validateFieldKey: (key: string): { valid: boolean; error?: string } => {
    if (!key) 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 };
  },

  // ===== Parsing =====
  extractUserInputFromFieldKey: (fieldKey: string | null | undefined): string => {
    if (!fieldKey) return '';
    // field_key 형식: {ID}_{사용자입력} → 사용자입력 부분만 추출
    const parts = fieldKey.split('_');
    return parts.length > 1 ? parts.slice(1).join('_') : fieldKey;
  },

  // ===== Transform =====
  toApiRequest: (formData: FieldFormData, sectionId: number) => ({
    section_id: sectionId,
    field_name: formData.name,
    field_key: formData.key,
    field_type: formData.inputType,
    is_required: formData.required,
    options: formData.options?.map(opt => ({ label: opt, value: opt })) || null,
    description: formData.description || null,
  }),

  toFormData: (field: ItemField): FieldFormData => ({
    name: field.field_name,
    key: fieldService.extractUserInputFromFieldKey(field.field_key),
    inputType: field.field_type,
    required: field.is_required,
    options: field.options?.map(opt => opt.value) || [],
    description: field.properties?.description || '',
  }),

  // ===== Defaults =====
  getDefaultValues: (): FieldFormData => ({
    name: '',
    key: '',
    inputType: 'textbox',
    required: false,
    options: [],
    description: '',
  }),
};
```

### 4.2 사용 예시 (Before → After)

```typescript
// Before: useFieldManagement.ts (중복 로직 직접 구현)
const handleAddField = async () => {
  // validation 직접 구현
  if (!newFieldName.trim()) {
    toast.error('필드명을 입력해주세요');
    return;
  }
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(newFieldKey)) {
    toast.error('필드 키는 영문자로 시작...');
    return;
  }

  // API 요청 직접 구성
  const fieldData = {
    section_id: sectionId,
    field_name: newFieldName,
    field_key: newFieldKey,
    // ...
  };

  await addFieldToSection(sectionId, fieldData);
};

// After: useFieldManagement.ts (서비스 호출)
import { fieldService } from '../services';

const handleAddField = async () => {
  // validation은 서비스에 위임
  const validation = fieldService.validate({ name: newFieldName, key: newFieldKey });
  if (!validation.valid) {
    Object.values(validation.errors).forEach(err => toast.error(err));
    return;
  }

  // API 요청도 서비스에서 생성
  const fieldData = fieldService.toApiRequest(
    { name: newFieldName, key: newFieldKey, inputType, required, options },
    sectionId
  );

  await addFieldToSection(sectionId, fieldData);
};
```

---

## 5. 리스크 및 대응

### 5.1 리스크

| 리스크 | 확률 | 영향 | 대응 |
|--------|-----|-----|------|
| 타입 에러 | 중 | 낮음 | 점진적 수정, 타입 체크 자주 실행 |
| 기존 동작 변경 | 낮음 | 중 | 각 Phase 후 기능 테스트 |
| 누락된 로직 | 중 | 낮음 | 서비스 미적용 파일은 기존 유지 |

### 5.2 롤백 전략

- Git 커밋을 Phase별로 분리
- 문제 발생 시 해당 Phase만 revert 가능

---

## 6. 작업 순서 권장

```
권장 순서 (의존성 + 난이도 고려):

1️⃣ fieldService 먼저 (가장 많이 중복, 효과 큼)
   → useFieldManagement, FieldDialog 적용
   → 테스트

2️⃣ masterFieldService (fieldService와 유사)
   → useMasterFieldManagement, MasterFieldDialog 적용
   → 테스트

3️⃣ pageService (독립적, 간단)
   → usePageManagement, PageDialog 적용
   → 테스트

4️⃣ sectionService
   → useSectionManagement, SectionDialog 적용
   → 테스트

5️⃣ templateService (가장 복잡)
   → useTemplateManagement, TemplateFieldDialog 적용
   → 테스트

6️⃣ attributeService (선택적)
   → useAttributeManagement, useTabManagement 적용
   → 테스트
```

---

## 7. 완료 기준

- [x] 모든 서비스 파일 생성 완료 ✅
- [x] validation 로직이 서비스로 통합됨 (중복 제거) ✅
- [x] 파싱 로직이 서비스로 통합됨 (중복 제거) ✅
- [x] 빌드 성공 ✅
- [ ] 기존 기능 동작 확인 (수동 테스트 필요)
- [x] 새 필드 추가 시 서비스 파일만 수정하면 되는 구조 확인 ✅

---

## 8. 관련 문서

- `[REF-2025-11-26] item-master-hooks-refactoring.md` - 훅 분리 작업 기록
- `[ANALYSIS] item-master-data-management.md` - 시스템 분석 문서