신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/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>
9.1 KiB
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.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
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 |
버그 상세 분석 |
향후 계획
-
백엔드 근본 수정:
getKnownFields()에item_details컬럼 추가- 이렇게 되면 고정 필드가
options에 중복 저장되지 않음
- 이렇게 되면 고정 필드가
-
고정 필드 동적화: 전개도 상세 등을 품목기준관리에서 동적 필드로 등록
- 이 경우
options에 저장되는 것이 정상
- 이 경우
-
프론트엔드 유지: 현재 패턴 (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 관련 유사 이슈