Files
sam-react-prod/claudedocs/guides/[GUIDE-2025-12-16] options-vs-flattened-data.md
byeongcheolryu c6b605200d feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지:
- 회계관리: 거래처, 예상비용, 청구서, 발주서
- 게시판: 공지사항, 자료실, 커뮤니티
- 고객센터: 문의/FAQ
- 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역
- 리포트 (차트 시각화)
- 개발자 테스트 URL 페이지

기능 개선:
- HR 직원관리/휴가관리/카드관리 강화
- IntegratedListTemplateV2 확장
- AuthenticatedLayout 패딩 표준화
- 로그인 페이지 UI 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 19:12:34 +09:00

9.1 KiB

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

개요

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

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

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

두 가지 저장 방식

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

options 배열 구조

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

평탄화된 응답 구조

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

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

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

발생 원인

백엔드 ItemService.phpgetKnownFields() 함수가 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

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;
}

잘못된 패턴 (이전 코드)

// ❌ 이렇게 하면 안됨!
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

// 필드 정의의 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 관련 유사 이슈