Files
sam-react-prod/claudedocs/item-master/[FIX-2025-12-16] options-details-duplicate-bug.md
byeongcheolryu b1587071f2 feat: 품목관리 기능 개선 및 문서화 업데이트
- 품목 상세/수정 페이지 파일 다운로드 기능 개선
- DynamicItemForm 파일 업로드 UI/UX 개선 (시방서, 인정서)
- BendingDiagramSection 조립/절곡 부품 전개도 통합
- API proxy route 품목 타입별 라우팅 개선
- ItemListClient 파일 다운로드 유틸리티 적용
- 품목코드 중복 체크 및 다이얼로그 추가

문서화:
- DynamicItemForm 훅 분리 계획서 추가 (2161줄 → 900줄 목표)
- 백엔드 API 마이그레이션 문서 추가
- 대용량 파일 처리 전략 가이드 추가
- 테넌트 데이터 격리 감사 문서 추가

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 11:01:25 +09:00

6.3 KiB

[FIX-2025-12-16] options vs item_details 중복 저장 버그

문제 현상

  • 품목 수정 시 bending_details 값이 111 → 1110으로 저장했는데, 다시 조회하면 111로 표시됨
  • 입력한 최신값이 아닌 이전에 저장된 값이 표시되는 문제

근본 원인

백엔드 저장 구조

┌─────────────────────────────────────────────────────────────┐
│                    백엔드 저장 구조                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  [동적 필드] ─────────────────────▶ options (JSON)         │
│  (품목기준관리에서 생성)              [{label, value}, ...]  │
│                                                             │
│  [고정 필드] ─────────────────────▶ item_details (컬럼)    │
│  (하드코딩: 시방서, 인정서, 전개도 등)   bending_details 등   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

문제 발생 지점

백엔드 ItemService.phpgetKnownFields() (85-109줄)

private function getKnownFields(): array
{
    // 1. 기본 고정 필드 (items 테이블 컬럼)
    $baseFields = [
        'id', 'tenant_id', 'item_type', 'code', 'name', 'unit', ...
    ];

    // 2. ItemField에서 source_table='items' AND storage_type='column' 인 것만 조회
    $columnFields = ItemField::where('source_table', 'items')  // ⚠️ items만!
        ->where('storage_type', 'column')
        ...

    // ❌ item_details 테이블 컬럼 누락!
    // ❌ SystemFields 상수 미사용!
}

결과: bending_detailsitem_details 컬럼이 "알려진 필드"로 인식되지 않음

데이터 흐름

저장 시:
  요청: { bending_details: 111 }
  → item_details.bending_details = 111  (extractDetailData로 저장)
  → options: [{label: "bending_details", value: "111"}]  (동적 필드로 잘못 인식)

수정 시 (값을 1110으로 변경):
  요청: { bending_details: 1110 }
  → item_details.bending_details = 1110  (최신값)
  → options: [{label: "bending_details", value: "1110"}]  (기존 값 덮어쓰기)

⚠️ 문제: options 병합 시 이전 값이 남아있는 경우가 있음

조회 시 (프론트엔드):
  API 응답: {
    bending_details: 1110,  // ← item_details에서 가져온 최신값
    options: [{label: "bending_details", value: "111"}]  // ← 첫 저장 시 값!
  }

  프론트엔드 매핑:
  1. formData.bending_details = 1110 (API 응답에서)
  2. options 순회: formData.bending_details = "111" ← 덮어쓰기! ❌

해결 방법

A. 프론트엔드 수정 (즉시 적용)

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

변경 내용:

  1. excludeKeys'details' 추가
  2. details 객체 펼치기 로직 추가 (최신값 매핑)
  3. options 순회 시 item_details 필드 제외
// 1. details는 아래에서 펼쳐서 추가
const excludeKeys = [..., 'details'];

// 2. details 객체가 있으면 펼쳐서 추가 (item_details 테이블 필드)
const details = (data as Record<string, unknown>).details;
if (details && typeof details === 'object') {
  const detailExcludeKeys = ['id', 'item_id', 'created_at', 'updated_at'];
  Object.entries(details).forEach(([key, value]) => {
    if (!detailExcludeKeys.includes(key) && value !== null && value !== undefined) {
      formData[key] = value;
    }
  });
}

// 3. options 순회 시 item_details 필드 제외
const detailsFieldsInOptions = [
  'files', 'bending_details', 'bending_diagram',
  'specification_file', 'certification_file',
];
if (data.options && Array.isArray(data.options)) {
  data.options.forEach((opt) => {
    if (opt.label && opt.value && !detailsFieldsInOptions.includes(opt.label)) {
      formData[opt.label] = opt.value;
    }
  });
}

B. 백엔드 수정 (근본 해결, 권장)

파일: /app/Services/ItemService.php - getKnownFields()

수정 요청:

// 방안 A: extractDetailData() 필드들을 knownFields에 추가
private function getKnownFields(): array
{
    $baseFields = [...];

    // item_details 테이블 필드 추가
    $detailFields = [
        'bending_diagram', 'bending_details',
        'specification_file', 'certification_file', ...
    ];

    return array_unique(array_merge($baseFields, $columnFields, $detailFields, $apiFields));
}

// 방안 B: SystemFields 상수 활용
use App\Constants\SystemFields;

private function getKnownFields(): array
{
    return array_unique(array_merge(
        $baseFields,
        $columnFields,
        SystemFields::getAllReservedKeysForGroup(SystemFields::GROUP_ITEM_MASTER),
        $apiFields
    ));
}

관련 파일

파일 역할
sam-api/app/Services/ItemService.php getKnownFields(), extractDetailData()
sam-api/app/Constants/SystemFields.php 시스템 필드 정의 (bending_details 포함)
sam-api/app/Models/Items/ItemDetail.php item_details 테이블 모델
sam-next/.../edit/page.tsx mapApiResponseToFormData()

영향 범위

  • bending_details (전개도 상세)
  • bending_diagram (전개도 이미지)
  • specification_file (시방서)
  • certification_file (인정서)
  • files (첨부파일)
  • 기타 item_details 테이블의 고정 컬럼들

향후 계획

  • 전개도 상세 입력 수치를 품목기준관리에서 동적 필드로 등록하는 형태로 변경 예정
  • 그때는 options에 저장되는 것이 정상
  • 현재는 하드코딩된 고정 필드이므로 item_details에만 저장되어야 함

참고

  • 유사 버그: files 필드도 같은 문제가 있었음 (2025-12-15 수정)
  • [GUIDE] radix-ui-select-controlled-mode-bug.md - Radix UI Select 관련 버그