# [GUIDE-2025-12-16] options vs 평탄화 데이터 패턴

## 개요

품목관리 시스템에서 백엔드 API 응답의 `options` 배열과 평탄화된 필드 데이터를 처리하는 패턴에 대한 가이드.

**핵심 원칙**: `options` 배열을 직접 파싱하지 말고, 백엔드가 정제해서 주는 평탄화된 필드만 사용한다.

## 배경: 백엔드 데이터 저장 구조

### 두 가지 저장 방식

```
┌─────────────────────────────────────────────────────────────────┐
│                      백엔드 저장 구조                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐                                           │
│  │   동적 필드      │ ────────▶  options (JSON 컬럼)           │
│  │ (품목기준관리에서 │            [{label, value}, ...]         │
│  │  동적으로 생성)  │                                           │
│  └─────────────────┘                                           │
│                                                                 │
│  ┌─────────────────┐                                           │
│  │   고정 필드      │ ────────▶  item_details 테이블 컬럼       │
│  │ (하드코딩된      │            bending_details,               │
│  │  시스템 필드)    │            specification_file 등          │
│  └─────────────────┘                                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### options 배열 구조

```json
{
  "options": [
    { "label": "custom_field_1", "value": "사용자 입력값1" },
    { "label": "custom_field_2", "value": "사용자 입력값2" }
  ]
}
```

### 평탄화된 응답 구조

백엔드가 조회 시 정제해서 내려주는 형태:

```json
{
  "id": 123,
  "code": "FG-001",
  "name": "제품명",
  "unit": "EA",
  "specification": "100x200",
  "details": {
    "bending_details": "1110",
    "specification_file": "https://..."
  },
  "options": [...]  // ← 이건 무시해야 함!
}
```

## 문제: options 직접 파싱의 위험성

### 발생 원인

백엔드 `ItemService.php`의 `getKnownFields()` 함수가 `item_details` 테이블 컬럼을 인식하지 못해서, 고정 필드도 `options`에 중복 저장되는 버그가 있었음.

### 데이터 흐름 (버그 상황)

```
[1차 저장] bending_details = 111
┌─────────────────────────────────────────────────────┐
│  item_details.bending_details = 111     ✅ 정상    │
│  options = [{label: "bending_details", value: "111"}]  ❌ 중복! │
└─────────────────────────────────────────────────────┘

[수정] bending_details = 1110
┌─────────────────────────────────────────────────────┐
│  item_details.bending_details = 1110    ✅ 최신값  │
│  options = [{label: "bending_details", value: "111"}]  ⚠️ 이전값! │
└─────────────────────────────────────────────────────┘

[조회 시 프론트엔드]
1. data.bending_details = 1110 가져옴     ✅
2. options 순회하며 덮어쓰기...
3. formData.bending_details = "111"       ❌ 이전값으로 덮어씀!
```

### 증상

- 품목 수정 후 다시 조회하면 이전 값이 표시됨
- 입력한 최신값이 저장은 되지만 화면에 반영 안됨
- 특히 `bending_details`, `specification_file`, `certification_file` 등에서 발생

## 해결: 평탄화 데이터만 사용

### 올바른 패턴 (현재 코드)

**파일**: `src/app/[locale]/(protected)/items/[id]/edit/page.tsx`

```typescript
function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData {
  const formData: DynamicFormData = {};

  // 1. 백엔드 응답의 최상위 필드를 그대로 복사
  Object.entries(data).forEach(([key, value]) => {
    if (!excludeKeys.includes(key) && value !== null) {
      formData[key] = value;
    }
  });

  // 2. details 객체 펼치기 (item_details 테이블 필드)
  const details = data.details;
  if (details && typeof details === 'object') {
    Object.entries(details).forEach(([key, value]) => {
      if (value !== null && value !== undefined) {
        formData[key] = value;  // 백엔드가 정제한 최신값
      }
    });
  }

  // 3. attributes 객체 펼치기 (동적 필드)
  const attributes = data.attributes || {};
  Object.entries(attributes).forEach(([key, value]) => {
    if (!(key in formData)) {  // 기존 값 덮어쓰지 않음
      formData[key] = value;
    }
  });

  // ❌ options 파싱 로직 제거!
  // options는 백엔드 내부 매핑용이므로 프론트엔드에서 사용하지 않음

  return formData;
}
```

### 잘못된 패턴 (이전 코드)

```typescript
// ❌ 이렇게 하면 안됨!
if (data.options && Array.isArray(data.options)) {
  data.options.forEach((opt) => {
    if (opt.label && opt.value) {
      formData[opt.label] = opt.value;  // stale 데이터로 덮어쓸 수 있음!
    }
  });
}
```

## 데이터 소스 우선순위

프론트엔드에서 폼 데이터 매핑 시 사용해야 할 데이터 소스 우선순위:

| 우선순위 | 데이터 소스 | 설명 |
|---------|------------|------|
| 1 | `data.details.*` | item_details 테이블의 고정 필드 (최신값 보장) |
| 2 | `data.*` (최상위) | items/products 테이블 필드 |
| 3 | `data.attributes.*` | 동적 필드 (품목기준관리에서 생성) |
| ❌ | `data.options[]` | 사용하지 않음 (내부 매핑용) |

## 영향받는 필드들

`options`에 잘못 저장될 수 있는 `item_details` 고정 필드들:

| 필드명 | 설명 | 저장 위치 |
|--------|------|----------|
| `bending_details` | 전개도 상세 정보 | item_details |
| `bending_diagram` | 전개도 이미지 URL | item_details |
| `specification_file` | 시방서 파일 URL | item_details |
| `certification_file` | 인정서 파일 URL | item_details |
| `files` | 첨부파일 목록 | item_details |

## DropdownField의 options 정규화

드롭다운 필드에서 사용하는 `options`는 **필드 정의의 선택지 목록**으로, 위에서 말하는 **품목 데이터의 options**와 다름.

**파일**: `src/components/items/DynamicItemForm/fields/DropdownField.tsx`

```typescript
// 필드 정의의 options (선택지 목록)를 정규화
function normalizeOptions(rawOptions: unknown): Array<{ label: string; value: string }> {
  // 문자열: "옵션1, 옵션2" → [{label, value}, ...]
  // 배열: ["옵션1", "옵션2"] → [{label, value}, ...]
  // 객체 배열: [{label, value}, ...] → 그대로 사용
}
```

이것은 필드 메타데이터의 선택지를 처리하는 것으로, 품목 데이터의 `options` 배열과 무관함.

## 관련 파일

| 파일 | 역할 |
|------|------|
| `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` | mapApiResponseToFormData() |
| `src/app/[locale]/(protected)/items/create/page.tsx` | 생성 페이지 (options 미사용) |
| `src/components/items/DynamicItemForm/fields/DropdownField.tsx` | 필드 정의 options 정규화 |
| `claudedocs/item-master/[FIX-2025-12-16] options-details-duplicate-bug.md` | 버그 상세 분석 |

## 향후 계획

1. **백엔드 근본 수정**: `getKnownFields()`에 `item_details` 컬럼 추가
   - 이렇게 되면 고정 필드가 `options`에 중복 저장되지 않음

2. **고정 필드 동적화**: 전개도 상세 등을 품목기준관리에서 동적 필드로 등록
   - 이 경우 `options`에 저장되는 것이 정상

3. **프론트엔드 유지**: 현재 패턴 (options 무시)은 백엔드 수정 후에도 안전함

## 체크리스트

새로운 API 응답 매핑 코드 작성 시:

- [ ] `options` 배열을 직접 파싱하지 않았는가?
- [ ] `details` 객체를 펼쳐서 최신값을 가져왔는가?
- [ ] `attributes` 객체 처리 시 기존 값을 덮어쓰지 않았는가?
- [ ] 고정 필드 (`bending_*`, `*_file`)가 올바른 소스에서 오는가?

## 참고

- `[FIX-2025-12-16] options-details-duplicate-bug.md` - 버그 발생 원인 상세 분석
- `[GUIDE] radix-ui-select-controlled-mode-bug.md` - Radix UI Select 관련 유사 이슈