1# 품목기준관리 시스템 분석 및 구현 계획

> **작성일**: 2025-11-17
> **소스**: React 프로젝트 ItemMasterDataManagement.tsx (1,413줄)
> **타겟**: Next.js 15 마이그레이션
> **상태**: 📊 분석 완료, 구현 대기

---

## 📑 목차

1. [프로젝트 배경 및 목표](#1-프로젝트-배경-및-목표)
2. [시스템 개요](#2-시스템-개요)
3. [데이터 구조 분석](#3-데이터-구조-분석)
4. [기능 분석](#4-기능-분석)
5. [API 요구사항](#5-api-요구사항)
6. [버전관리 시스템](#6-버전관리-시스템)
7. [멀티테넌시 및 데이터 로딩 전략](#7-멀티테넌시-및-데이터-로딩-전략)
8. [Next.js 15 마이그레이션 계획](#8-nextjs-15-마이그레이션-계획)
9. [구현 우선순위](#9-구현-우선순위)
10. [다음 단계](#10-다음-단계)

---

## 1. 프로젝트 배경 및 목표

### 1.1 배경

현재 구현된 **품목등록 화면** (`ItemForm.tsx`)은 고정된 값(템플릿)으로 구성되어 있습니다.
실제 운영 환경에서는 이 고정값들을 **동적으로 설정/편집**할 수 있어야 하며,
이를 위한 별도의 **품목기준관리 페이지**가 필요합니다.

```
품목기준관리 페이지 (ItemMasterDataManagement)
    ↓ 설정/편집
품목등록 화면 (ItemForm)
    ↓ 사용자 입력
실제 품목 데이터
```

### 1.2 핵심 요구사항

1. ✅ **드롭다운 옵션 관리**: 단위, 재질, 표면처리 등
2. ✅ **페이지 구조 관리**: 섹션, 하위섹션, 항목 계층 구조
3. ✅ **마스터 항목 템플릿**: 재사용 가능한 항목 정의
4. ✅ **조건부 표시 로직**: 특정 값에 따라 항목 표시/숨김
5. ✅ **품목 코드 자동생성 규칙**: 동적으로 설정 가능한 규칙 체계
6. ✅ **다양한 업종 지원**: 고객사별 자유로운 페이지 구성
7. ✅ **버전관리 시스템**: 품목 수정 이력 추적 및 이전 버전 조회

### 1.3 목표

**1차 목표**: 품목기준관리 페이지 구축 및 Laravel API 연동
**2차 목표**: 품목등록 화면을 동적 템플릿 시스템으로 전환 (선택적)

---

## 2. 시스템 개요

### 2.1 React 프로젝트 파일 정보

**파일 경로**: `/sma-react-v2.0/src/components/ItemMasterDataManagement.tsx`
**파일 크기**: 1,413줄
**주요 의존성**:
- `DataContext` - 전역 상태 관리 (6,697줄)
- `SpecificationManagement` - 규격 관리 서브 컴포넌트
- `DraggableField` - 드래그 앤 드롭 항목

### 2.2 6개 탭 구조

```
┌─────────────────────────────────────────────────────────────┐
│                     품목기준관리 메인 화면                    │
└─────────────────────────────────────────────────────────────┘
    │
    ├─ 📊 계층구조 (hierarchy)
    │   └─ 섹션 > 하위섹션 > 항목 트리 구조
    │
    ├─ 📋 항목 (items)
    │   └─ 마스터 항목 템플릿 (제품/부품/부자재/원자재별)
    │
    ├─ 📏 단위 (units)
    │   └─ EA, SET, KG, M, L, BOX, PCS 등
    │
    ├─ 🧱 재질 (materials)
    │   └─ EGI 1.2T, SUS 1.2T, SPHC-SD 등
    │
    ├─ 🎨 표면처리 (surface)
    │   └─ 무도장, 파우더도장, 아연도금 등
    │
    └─ 📐 규격 (specifications)
        └─ 별도 SpecificationManagement 컴포넌트
```

### 2.3 데이터 흐름

```
사용자
   ↓
품목기준관리 페이지
   ↓ 설정 저장
Laravel API (DB)
   ↓ 조회
품목등록 화면 (ItemForm)
   ↓ 드롭다운, 필드 옵션 표시
사용자 입력
```

---

## 3. 데이터 구조 분석

### 3.1 계층구조 데이터

#### ItemPage (섹션)

```typescript
interface ItemPage {
  id: string;                        // 예: "PAGE-1234567890"
  pageName: string;                  // 예: "품목 등록"
  itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'; // 품목 유형
  sections: ItemSection[];           // 하위섹션 배열
  isActive: boolean;                 // 활성 여부
  absolutePath?: string;             // 절대경로 (선택적)
  createdAt: string;                 // 생성일
  updatedAt?: string;                // 수정일
}
```

**예시**:
```json
{
  "id": "PAGE-001",
  "pageName": "제품(FG) 등록",
  "itemType": "FG",
  "sections": [...],
  "isActive": true,
  "createdAt": "2025-01-10"
}
```

#### ItemSection (하위섹션)

```typescript
interface ItemSection {
  id: string;                        // 예: "SECTION-1234567890"
  title: string;                     // 예: "기본정보"
  description?: string;              // 설명 (선택)
  category?: string[];               // 카테고리 조건 (선택)
  fields: ItemField[];               // 항목 배열
  type?: 'fields' | 'bom';           // 섹션 타입 (선택)
  order: number;                     // 표시 순서
  isCollapsible: boolean;            // 접기/펼치기 가능 여부
  isCollapsed: boolean;              // 기본 접힘 상태
  createdAt: string;                 // 생성일
}
```

**예시**:
```json
{
  "id": "SECTION-001",
  "title": "기본정보",
  "description": "품목의 기본 정보를 입력합니다",
  "fields": [...],
  "order": 1,
  "isCollapsible": true,
  "isCollapsed": false,
  "createdAt": "2025-01-10"
}
```

#### ItemField (항목)

```typescript
interface ItemField {
  id: string;                        // 예: "FIELD-1234567890"
  name: string;                      // 예: "품목코드"
  fieldKey: string;                  // 예: "itemCode"
  property: ItemFieldProperty;       // 입력 속성
  description?: string;              // 설명 (선택)
  displayCondition?: FieldDisplayCondition; // 조건부 표시 (선택)
  masterFieldId?: string;            // 마스터 항목 ID (선택)
  order?: number;                    // 표시 순서 (선택)
  createdAt: string;                 // 생성일
}
```

**예시**:
```json
{
  "id": "FIELD-001",
  "name": "품목코드",
  "fieldKey": "itemCode",
  "property": {
    "inputType": "textbox",
    "required": true,
    "row": 1,
    "col": 1
  },
  "description": "품목을 식별하는 고유 코드",
  "createdAt": "2025-01-10"
}
```

#### ItemFieldProperty (입력 속성)

```typescript
interface ItemFieldProperty {
  inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
  required: boolean;                 // 필수 여부
  row: number;                       // 레이아웃 행
  col: number;                       // 레이아웃 열
  options?: string[];                // 드롭다운 옵션 (dropdown일 때)
}
```

**예시 (드롭다운)**:
```json
{
  "inputType": "dropdown",
  "required": true,
  "row": 1,
  "col": 1,
  "options": ["EA", "SET", "KG", "M"]
}
```

#### FieldDisplayCondition (조건부 표시)

```typescript
interface FieldDisplayCondition {
  fieldKey: string;                  // 조건 필드 키
  expectedValue: string;             // 기대 값
}
```

**예시**:
```json
{
  "fieldKey": "itemType",
  "expectedValue": "FG"
}
```
**의미**: `itemType` 필드 값이 `"FG"`일 때만 이 항목을 표시

---

### 3.2 마스터 항목 데이터

#### ItemMasterField

```typescript
interface ItemMasterField {
  id: string;                        // 예: "MASTER-1234567890"
  name: string;                      // 예: "품목코드"
  fieldKey: string;                  // 예: "itemCode"
  property: ItemFieldProperty;       // 입력 속성
  category?: string;                 // 카테고리 (예: "공통", "제품", "부품")
  description?: string;              // 설명
  isActive: boolean;                 // 활성 여부
  createdAt: string;                 // 생성일
}
```

**역할**:
- 재사용 가능한 항목 템플릿
- 여러 섹션/페이지에서 참조 가능
- 품목 분류별로 관리 (공통, 제품, 부품, 부자재, 원자재)

**예시**:
```json
{
  "id": "MASTER-001",
  "name": "품목코드",
  "fieldKey": "itemCode",
  "property": {
    "inputType": "textbox",
    "required": true,
    "row": 1,
    "col": 1
  },
  "category": "공통",
  "description": "모든 품목에 공통으로 사용되는 품목코드",
  "isActive": true,
  "createdAt": "2025-01-10"
}
```

---

### 3.3 옵션 데이터 (단위/재질/표면처리)

#### MasterOption

```typescript
interface MasterOption {
  id: string;                        // 예: "unit-1", "mat-1", "surf-1"
  value: string;                     // 코드 값 (예: "EA", "EGI 1.2T")
  label: string;                     // 표시명 (예: "EA (개)", "EGI 1.2T")
  isActive: boolean;                 // 활성 여부
}
```

**관리 대상**:
1. **단위 (units)**: `EA`, `SET`, `KG`, `M`, `L`, `BOX`, `PCS`
2. **재질 (materials)**: `EGI 1.2T`, `EGI 2.0T`, `SUS 1.2T`, `SPHC-SD 1.6T`
3. **표면처리 (surface)**: `무도장`, `파우더도장`, `아연도금`

**예시 (단위)**:
```json
[
  { "id": "unit-1", "value": "EA", "label": "EA (개)", "isActive": true },
  { "id": "unit-2", "value": "SET", "label": "SET (세트)", "isActive": true },
  { "id": "unit-3", "value": "KG", "label": "KG (킬로그램)", "isActive": true }
]
```

---

### 3.4 로컬스토리지 키 구조

React 프로젝트에서는 로컬스토리지를 사용하지만,
Next.js에서는 **Laravel API**로 대체합니다.

**현재 (React)**:
```javascript
const UNIT_OPTIONS_KEY = 'unit-options';
const MATERIAL_OPTIONS_KEY = 'material-options';
const SURFACE_TREATMENT_OPTIONS_KEY = 'surface-treatment-options';
const ITEM_CLASSIFICATIONS_KEY = 'item-classifications';
```

**목표 (Next.js + Laravel)**:
```
GET /api/master-data/units
GET /api/master-data/materials
GET /api/master-data/surface-treatments
GET /api/master-data/pages
GET /api/master-data/fields
```

---

## 4. 기능 분석

### 4.1 계층구조 탭

**주요 기능**:
1. ✅ **섹션 생성**: 품목 유형별 페이지 생성 (FG/PT/SM/RM/CS)
2. ✅ **하위섹션 추가**: 섹션 내 그룹 추가 (기본정보, BOM, 가격정보 등)
3. ✅ **항목 추가/편집**: 입력 필드 정의 및 수정
4. ✅ **드래그 앤 드롭**: 항목 순서 변경
5. ✅ **조건부 표시**: 특정 값에 따라 항목 표시/숨김
6. ✅ **마스터 항목 연동**: 마스터 항목 템플릿 선택

**UI 구조**:
```
┌─────────────────────────────────────────────────────┐
│  섹션 목록 (좌측)     │   계층구조 상세 (우측)       │
│                       │                             │
│  - 제품(FG) 등록      │   ┌─ 기본정보 섹션          │
│  - 부품(PT) 등록      │   │  - 품목코드             │
│  - 부자재(SM) 등록    │   │  - 품목명               │
│  - 원자재(RM) 등록    │   │  - 단위                 │
│  - 소모품(CS) 등록    │   │  [+ 항목 추가]          │
│                       │   │                         │
│  [+ 섹션 추가]        │   └─ BOM 섹션               │
│                       │      - 하위 품목코드         │
│                       │      - 수량                 │
│                       │      [+ 항목 추가]          │
└─────────────────────────────────────────────────────┘
```

**항목 추가 다이얼로그**:
- 마스터 항목에서 불러오기 (토글)
- 항목명, 필드 키
- 입력 방식 (텍스트박스, 드롭다운, 체크박스, 숫자, 날짜, 텍스트영역)
- 필수 항목 여부
- 드롭다운 옵션 (쉼표로 구분)
- 조건부 항목 설정 (조건 필드, 조건 값)
- 설명

---

### 4.2 항목 탭

**주요 기능**:
1. ✅ **마스터 항목 추가/편집/삭제**
2. ✅ **품목 분류별 필터링**: 공통, 제품, 부품, 부자재, 원자재
3. ✅ **재사용 가능한 템플릿 관리**

**UI 구조**:
```
┌─────────────────────────────────────────────────────┐
│  마스터 항목 관리                    [+ 항목 추가]   │
├─────────────────────────────────────────────────────┤
│  [제품] [부품] [부자재] [원자재]  (서브 탭)         │
├─────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────┐   │
│  │ 품목코드  [텍스트박스] [필수] [공통]         │   │
│  │ 필드키: itemCode                       [편집][삭제]│
│  └─────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────┐   │
│  │ 단위  [드롭다운] [필수] [공통]               │   │
│  │ 필드키: unit                           [편집][삭제]│
│  │ 옵션: EA, SET, KG, M                         │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
```

---

### 4.3 단위/재질/표면처리 탭

**공통 기능**:
1. ✅ **옵션 추가**: 값(코드), 라벨(표시명)
2. ✅ **옵션 삭제**
3. ✅ **활성/비활성 관리**

**UI 구조** (단위 탭 예시):
```
┌─────────────────────────────────────────────────────┐
│  단위 관리                               [+ 추가]    │
├─────────────────────────────────────────────────────┤
│  번호 │ 값 (Value) │ 라벨 (Label)     │ 작업       │
│  1    │ EA         │ EA (개)          │ [삭제]     │
│  2    │ SET        │ SET (세트)       │ [삭제]     │
│  3    │ KG         │ KG (킬로그램)    │ [삭제]     │
└─────────────────────────────────────────────────────┘
```

---

### 4.4 규격 탭

**구현**:
- 별도 `SpecificationManagement` 컴포넌트
- React 프로젝트에서 추가 분석 필요

---

## 5. API 요구사항

### 5.1 계층구조 관리 API

#### 섹션 (ItemPage) 관리

```typescript
// 섹션 목록 조회
GET /api/master-data/pages
Response: {
  success: true,
  data: ItemPage[]
}

// 섹션 생성
POST /api/master-data/pages
Request: {
  pageName: string,
  itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS'
}
Response: {
  success: true,
  data: ItemPage
}

// 섹션 수정
PUT /api/master-data/pages/:pageId
Request: {
  pageName?: string,
  isActive?: boolean
}

// 섹션 삭제
DELETE /api/master-data/pages/:pageId
```

#### 하위섹션 (ItemSection) 관리

```typescript
// 하위섹션 추가
POST /api/master-data/pages/:pageId/sections
Request: {
  title: string,
  description?: string,
  order: number,
  isCollapsible: boolean,
  isCollapsed: boolean
}
Response: {
  success: true,
  data: ItemSection
}

// 하위섹션 수정
PUT /api/master-data/pages/:pageId/sections/:sectionId
Request: {
  title?: string,
  description?: string,
  order?: number
}

// 하위섹션 삭제
DELETE /api/master-data/pages/:pageId/sections/:sectionId
```

#### 항목 (ItemField) 관리

```typescript
// 항목 추가
POST /api/master-data/pages/:pageId/sections/:sectionId/fields
Request: {
  name: string,
  fieldKey: string,
  property: ItemFieldProperty,
  description?: string,
  displayCondition?: FieldDisplayCondition,
  masterFieldId?: string,
  order?: number
}
Response: {
  success: true,
  data: ItemField
}

// 항목 수정
PUT /api/master-data/pages/:pageId/sections/:sectionId/fields/:fieldId
Request: {
  name?: string,
  fieldKey?: string,
  property?: ItemFieldProperty,
  description?: string,
  displayCondition?: FieldDisplayCondition
}

// 항목 순서 변경
PUT /api/master-data/pages/:pageId/sections/:sectionId/fields/reorder
Request: {
  fieldIds: string[]  // 새로운 순서의 필드 ID 배열
}

// 항목 삭제
DELETE /api/master-data/pages/:pageId/sections/:sectionId/fields/:fieldId
```

---

### 5.2 마스터 항목 API

```typescript
// 마스터 항목 목록 조회 (카테고리별)
GET /api/master-data/fields?category=공통
GET /api/master-data/fields?category=제품
GET /api/master-data/fields?category=부품
Response: {
  success: true,
  data: ItemMasterField[]
}

// 마스터 항목 생성
POST /api/master-data/fields
Request: {
  name: string,
  fieldKey: string,
  property: ItemFieldProperty,
  category?: string,
  description?: string
}
Response: {
  success: true,
  data: ItemMasterField
}

// 마스터 항목 수정
PUT /api/master-data/fields/:fieldId
Request: {
  name?: string,
  fieldKey?: string,
  property?: ItemFieldProperty,
  category?: string,
  description?: string
}

// 마스터 항목 삭제
DELETE /api/master-data/fields/:fieldId
```

---

### 5.3 옵션 관리 API

```typescript
// 단위 목록 조회
GET /api/master-data/units
Response: {
  success: true,
  data: MasterOption[]
}

// 단위 추가
POST /api/master-data/units
Request: {
  value: string,
  label: string
}

// 단위 삭제
DELETE /api/master-data/units/:unitId

// 재질 목록 조회
GET /api/master-data/materials
Response: {
  success: true,
  data: MasterOption[]
}

// 재질 추가
POST /api/master-data/materials
Request: {
  value: string,
  label: string
}

// 재질 삭제
DELETE /api/master-data/materials/:materialId

// 표면처리 목록 조회
GET /api/master-data/surface-treatments
Response: {
  success: true,
  data: MasterOption[]
}

// 표면처리 추가
POST /api/master-data/surface-treatments
Request: {
  value: string,
  label: string
}

// 표면처리 삭제
DELETE /api/master-data/surface-treatments/:surfaceId
```

---

### 5.4 통합 조회 API

품목등록 화면에서 사용할 마스터 데이터를 한 번에 조회:

```typescript
GET /api/master-data
Response: {
  success: true,
  data: {
    units: MasterOption[],
    materials: MasterOption[],
    surfaceTreatments: MasterOption[],
    pages: ItemPage[],
    masterFields: ItemMasterField[]
  }
}
```

---

## 6. 버전관리 시스템

### 6.1 시스템 개요

품목기준관리 시스템은 **포괄적인 버전관리 시스템**을 포함하여 모든 데이터 수정 이력을 추적합니다.
이는 단순한 감사 로그가 아니라, 이전 버전으로의 롤백과 변경 이력 비교가 가능한 **완전한 버전 관리** 시스템입니다.

**핵심 특징**:
- ✅ **전체 데이터 스냅샷**: 수정 시마다 이전 상태 전체를 저장
- ✅ **필수 수정 사유**: 모든 수정 작업에 사유(revision reason) 입력 강제
- ✅ **버전 번호 자동 증가**: currentRevision 필드로 현재 버전 추적
- ✅ **이력 조회**: 특정 품목의 전체 수정 이력 확인
- ✅ **이전 버전 복원**: previousData를 활용한 롤백 기능
- ✅ **다중 엔티티 지원**: 품목, 견적, 수주, 계산식, 단가 등 시스템 전반 적용

**버전관리 대상 엔티티**:
```
✅ ItemMaster (품목)
✅ Quote (견적)
✅ Order (수주)
✅ CalculationFormula (계산식)
✅ PricingData (단가)
✅ FormulaRule (수식 규칙)
```

---

### 6.2 데이터 구조

#### ItemRevision (품목 수정 이력)

```typescript
export interface ItemRevision {
  revisionNumber: number;           // 수정 버전 번호 (1부터 시작)
  revisionDate: string;              // 수정 날짜 (ISO 8601 형식)
  revisionBy: string;                // 수정자 (사용자 ID 또는 이름)
  revisionReason?: string;           // 수정 사유 (필수)
  changedFields?: string[];          // 변경된 필드 목록 (선택)
  previousData: any;                 // 이전 상태 전체 스냅샷
}
```

**예시**:
```json
{
  "revisionNumber": 3,
  "revisionDate": "2025-01-15T10:30:00Z",
  "revisionBy": "admin@example.com",
  "revisionReason": "품목코드 생성 규칙 변경",
  "changedFields": ["autoGenerationRule", "codePattern"],
  "previousData": {
    "id": "PAGE-001",
    "pageName": "제품(FG) 등록",
    "itemType": "FG",
    "sections": [...],
    "currentRevision": 2,
    "revisions": [...]
  }
}
```

---

### 6.3 버전관리 필드

모든 버전관리 대상 엔티티는 다음 필드를 포함합니다:

```typescript
interface VersionedEntity {
  // ... 기본 필드들

  // 버전관리 필드
  currentRevision: number;           // 현재 버전 번호 (기본값: 0)
  revisions: ItemRevision[];         // 수정 이력 배열
  isFinal?: boolean;                 // 최종 확정 여부 (선택)
}
```

**예시 (ItemPage with Version)**:
```typescript
export interface ItemPage {
  id: string;
  pageName: string;
  itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
  sections: ItemSection[];
  isActive: boolean;
  absolutePath?: string;
  createdAt: string;
  updatedAt?: string;

  // 버전관리 필드
  currentRevision: number;           // 현재 버전 (예: 3)
  revisions: ItemRevision[];         // 수정 이력 배열
  isFinal?: boolean;                 // 최종 확정 여부
}
```

---

### 6.4 버전관리 워크플로우

#### 수정 프로세스

```
사용자가 수정 요청
    ↓
수정 사유 다이얼로그 표시 (필수)
    ↓
사용자가 수정 사유 입력
    ↓
현재 데이터 전체를 previousData로 저장
    ↓
currentRevision 증가 (예: 2 → 3)
    ↓
revisions 배열에 새 ItemRevision 추가
    ↓
수정된 데이터 저장
    ↓
성공 메시지 표시
```

**React 프로젝트 코드 예시** (ItemManagement.tsx:1637-1651):
```typescript
// 버전 관리 - 수정 시 이력 저장
const currentRevision = selectedItem.currentRevision || 0;
const newRevisionNumber = currentRevision + 1;

const revision = {
  revisionNumber: newRevisionNumber,
  revisionDate: new Date().toISOString().split("T")[0],
  revisionBy: "관리자", // TODO: 실제 사용자 정보로 교체
  revisionReason: revisionReason,
  previousData: { ...selectedItem }
};

pendingUpdates.currentRevision = newRevisionNumber;
pendingUpdates.revisions = [...(selectedItem.revisions || []), revision];
```

---

### 6.5 UI/UX 구성

#### 수정 사유 입력 다이얼로그

```tsx
<Dialog open={isRevisionDialogOpen}>
  <DialogHeader>
    <DialogTitle>수정 사유 입력</DialogTitle>
    <DialogDescription>
      데이터를 수정하는 이유를 입력해주세요. 이 정보는 수정 이력에 기록됩니다.
    </DialogDescription>
  </DialogHeader>

  <div className="space-y-4">
    <Textarea
      placeholder="수정 사유를 입력하세요 (필수)"
      value={revisionReason}
      onChange={(e) => setRevisionReason(e.target.value)}
      required
    />
  </div>

  <DialogFooter>
    <Button variant="outline" onClick={cancelRevision}>
      취소
    </Button>
    <Button
      onClick={confirmRevision}
      disabled={!revisionReason.trim()}
    >
      확인 및 저장
    </Button>
  </DialogFooter>
</Dialog>
```

#### 수정 이력 조회 UI

```
┌─────────────────────────────────────────────────────────────┐
│  품목 수정 이력                              [이력 보기]     │
├─────────────────────────────────────────────────────────────┤
│  버전 │ 수정일      │ 수정자       │ 수정 사유             │
│  3    │ 2025-01-15 │ admin       │ 품목코드 규칙 변경     │
│  2    │ 2025-01-10 │ manager     │ 단위 옵션 추가        │
│  1    │ 2025-01-05 │ admin       │ 초기 생성             │
└─────────────────────────────────────────────────────────────┘

[특정 버전 클릭 시]
┌─────────────────────────────────────────────────────────────┐
│  버전 2 상세 정보                                [이전으로]  │
├─────────────────────────────────────────────────────────────┤
│  수정 번호: 2                                               │
│  수정일: 2025-01-10                                         │
│  수정자: manager                                            │
│  수정 사유: 단위 옵션 추가                                  │
│                                                             │
│  변경 내용:                                                 │
│  - 섹션 "기본정보" > 항목 "단위" 옵션 추가: "BOX", "PCS"   │
│                                                             │
│  [이 버전으로 복원]  [변경 내용 비교]                       │
└─────────────────────────────────────────────────────────────┘
```

---

### 6.6 API 요구사항

#### 수정 이력 조회 API

```typescript
// 특정 품목의 수정 이력 조회
GET /api/master-data/pages/:pageId/revisions
Response: {
  success: true,
  data: {
    currentRevision: number,
    revisions: ItemRevision[]
  }
}

// 특정 버전 상세 조회
GET /api/master-data/pages/:pageId/revisions/:revisionNumber
Response: {
  success: true,
  data: {
    revision: ItemRevision,
    current: ItemPage,
    previous: ItemPage  // previousData에서 복원
  }
}
```

#### 버전 복원 API

```typescript
// 특정 버전으로 롤백
POST /api/master-data/pages/:pageId/restore
Request: {
  revisionNumber: number,
  revisionReason: string  // 복원 사유 (필수)
}
Response: {
  success: true,
  data: ItemPage  // 복원된 데이터
}
```

#### 수정 시 버전 생성

```typescript
// 모든 PUT 요청에 revisionReason 필수
PUT /api/master-data/pages/:pageId
Request: {
  pageName?: string,
  isActive?: boolean,
  revisionReason: string  // 필수!
}
Response: {
  success: true,
  data: {
    ...updatedPage,
    currentRevision: 3,
    revisions: [...]
  }
}
```

---

### 6.7 데이터베이스 스키마

#### 버전관리 필드 추가 (기존 테이블 확장)

```sql
-- item_pages 테이블에 버전 필드 추가
ALTER TABLE item_pages
ADD COLUMN current_revision INT DEFAULT 0,
ADD COLUMN revisions JSON,
ADD COLUMN is_final BOOLEAN DEFAULT FALSE;

-- item_master_fields 테이블에 버전 필드 추가
ALTER TABLE item_master_fields
ADD COLUMN current_revision INT DEFAULT 0,
ADD COLUMN revisions JSON,
ADD COLUMN is_final BOOLEAN DEFAULT FALSE;

-- 옵션 테이블들에도 동일하게 적용
ALTER TABLE master_units
ADD COLUMN current_revision INT DEFAULT 0,
ADD COLUMN revisions JSON;

ALTER TABLE master_materials
ADD COLUMN current_revision INT DEFAULT 0,
ADD COLUMN revisions JSON;

ALTER TABLE master_surface_treatments
ADD COLUMN current_revision INT DEFAULT 0,
ADD COLUMN revisions JSON;
```

#### 수정 이력 JSON 구조 예시

```json
{
  "revisions": [
    {
      "revisionNumber": 1,
      "revisionDate": "2025-01-05T09:00:00Z",
      "revisionBy": "admin@example.com",
      "revisionReason": "초기 생성",
      "previousData": null
    },
    {
      "revisionNumber": 2,
      "revisionDate": "2025-01-10T14:30:00Z",
      "revisionBy": "manager@example.com",
      "revisionReason": "단위 옵션 추가",
      "changedFields": ["sections[0].fields[2].property.options"],
      "previousData": {
        "id": "PAGE-001",
        "pageName": "제품(FG) 등록",
        "sections": [...]
      }
    },
    {
      "revisionNumber": 3,
      "revisionDate": "2025-01-15T10:30:00Z",
      "revisionBy": "admin@example.com",
      "revisionReason": "품목코드 생성 규칙 변경",
      "changedFields": ["autoGenerationRule"],
      "previousData": {
        "id": "PAGE-001",
        "pageName": "제품(FG) 등록",
        "sections": [...],
        "currentRevision": 2
      }
    }
  ]
}
```

---

### 6.8 구현 고려사항

#### 성능 최적화

**문제**: `previousData`에 전체 객체를 저장하면 데이터베이스 용량이 빠르게 증가

**해결 방안**:
```typescript
// 옵션 1: 변경된 필드만 저장 (delta 방식)
{
  revisionNumber: 3,
  changedFields: ["pageName", "sections[0].title"],
  delta: {
    pageName: { from: "제품 등록", to: "제품(FG) 등록" },
    "sections[0].title": { from: "기본 정보", to: "기본정보" }
  }
}

// 옵션 2: 압축 저장 (gzip)
{
  revisionNumber: 3,
  previousData: "H4sIAAAAAAAAA..." // gzipped base64
}

// 옵션 3: 주요 버전만 전체 저장
{
  revisionNumber: 3,
  fullSnapshot: false,  // 매 10번째 버전만 true
  changedFields: ["pageName"],
  delta: {...}
}
```

#### 보안 및 권한

```typescript
// 버전 관리 권한 체크
interface RevisionPermissions {
  canView: boolean;      // 수정 이력 조회
  canRestore: boolean;   // 이전 버전 복원
  canDelete: boolean;    // 이력 삭제 (관리자만)
}

// API 요청 시 권한 확인
if (!user.hasPermission('VIEW_REVISION_HISTORY')) {
  throw new ForbiddenException('수정 이력을 조회할 권한이 없습니다');
}
```

#### 이력 보존 정책

```typescript
// 데이터 보존 정책 설정
interface RetentionPolicy {
  maxRevisions: number;       // 최대 보관 이력 수 (예: 100)
  retentionDays: number;      // 보관 기간 (예: 365일)
  autoArchive: boolean;       // 자동 아카이브 여부
  archiveAfterDays: number;   // 아카이브 시점 (예: 180일)
}

// 오래된 이력 자동 정리
async function cleanupOldRevisions(pageId: string, policy: RetentionPolicy) {
  const page = await fetchPage(pageId);

  // 최대 이력 수 초과 시 가장 오래된 것부터 제거
  if (page.revisions.length > policy.maxRevisions) {
    page.revisions = page.revisions.slice(-policy.maxRevisions);
  }

  // 보존 기간 지난 이력 아카이브
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - policy.archiveAfterDays);

  page.revisions = page.revisions.filter(
    r => new Date(r.revisionDate) > cutoffDate
  );
}
```

---

### 6.9 시스템 전반 적용 범위

버전관리 시스템은 품목기준관리뿐만 아니라 **시스템 전체**에 적용됩니다:

**적용 대상**:
```yaml
item_management:
  - ItemMaster (품목)
  - ItemPage (품목기준관리 페이지)
  - ItemMasterField (마스터 항목)

quote_management:
  - Quote (견적)
  - QuoteTemplate (견적 템플릿)

order_management:
  - Order (수주)
  - OrderTemplate (수주 템플릿)

calculation:
  - CalculationFormula (계산식)
  - FormulaRule (수식 규칙)

pricing:
  - PricingData (단가)
  - PriceTemplate (단가 템플릿)

master_data:
  - MasterUnits (단위)
  - MasterMaterials (재질)
  - MasterSurfaceTreatments (표면처리)
```

**공통 Hook 구현** (React 프로젝트 참조):
```typescript
// useVersionControl.ts
export function useVersionControl(
  entityName: string,
  filePath: string
) {
  const [isVersionDialogOpen, setIsVersionDialogOpen] = useState(false);
  const [revisionReason, setRevisionReason] = useState("");
  const [pendingUpdates, setPendingUpdates] = useState<any>(null);

  const requestVersionUpdate = (updates: any) => {
    setPendingUpdates(updates);
    setIsVersionDialogOpen(true);
  };

  const confirmVersionUpdate = async () => {
    if (!revisionReason.trim()) {
      alert("수정 사유를 입력해주세요");
      return;
    }

    // 버전 업데이트 로직
    const currentRevision = entity.currentRevision || 0;
    const newRevisionNumber = currentRevision + 1;

    const revision = {
      revisionNumber: newRevisionNumber,
      revisionDate: new Date().toISOString(),
      revisionBy: getCurrentUser(),
      revisionReason: revisionReason,
      previousData: { ...entity }
    };

    const updated = {
      ...entity,
      ...pendingUpdates,
      currentRevision: newRevisionNumber,
      revisions: [...(entity.revisions || []), revision]
    };

    await saveEntity(updated);

    setIsVersionDialogOpen(false);
    setRevisionReason("");
    setPendingUpdates(null);
  };

  return {
    isVersionDialogOpen,
    setIsVersionDialogOpen,
    revisionReason,
    setRevisionReason,
    requestVersionUpdate,
    confirmVersionUpdate
  };
}
```

---

## 7. 멀티테넌시 및 데이터 로딩 전략

> **📌 상세 구현 가이드**: [[REF-2025-11-19] multi-tenancy-implementation.md](./%5BREF-2025-11-19%5D%20multi-tenancy-implementation.md)
>
> 실제 로그인 응답 구조(tenant.id) 기반 구현 방법, TenantAwareCache 유틸리티, Phase별 로드맵 등 상세 내용 참고

### 7.1 멀티테넌시 개요

**핵심 요구사항**: 테넌트(고객사)별로 품목기준관리 구성이 다르게 설정되어야 함

```
┌─────────────────────────────────────────────────────────────┐
│                      멀티테넌시 구조                          │
└─────────────────────────────────────────────────────────────┘

테넌트 A (제조업)                테넌트 B (유통업)
├─ 품목 유형: FG, PT, RM         ├─ 품목 유형: FG, SM
├─ 필수 필드: 재질, 표면처리     ├─ 필수 필드: 바코드, 원산지
├─ 섹션 구성: 생산정보 포함      ├─ 섹션 구성: 유통정보 포함
└─ 단위: EA, KG, M               └─ 단위: EA, BOX, PCS
```

**구현 방식**:
- 사용자 로그인 시 `tenant_id` 포함
- 모든 API 요청에 테넌트 컨텍스트 자동 포함
- 데이터베이스에서 테넌트별 필터링

---

### 7.2 데이터 로딩 전략: 하이브리드 방식

#### 전략 비교

| 방식 | 초기 로딩 | 페이지 전환 | 데이터 최신성 | 메모리 사용 |
|------|----------|------------|-------------|-----------|
| Eager Loading | 느림 ❌ | 빠름 ✅ | 낮음 ❌ | 높음 ❌ |
| Lazy Loading | 빠름 ✅ | 느림 ❌ | 높음 ✅ | 낮음 ✅ |
| **Hybrid (채택)** | **빠름 ✅** | **빠름 ✅** | **높음 ✅** | **낮음 ✅** |

#### 하이브리드 방식 구조

**1단계: 로그인 시 기본 정보만 로드**
```typescript
// POST /api/auth/login 응답
{
  user: {
    id: "user123",
    name: "홍길동",
    tenant_id: "tenant_001"  // 테넌트 식별자
  },
  menu: [...],  // 메뉴 구조만 (구성 데이터 X)
  roles: [...],
  tenant: {
    id: "tenant_001",
    name: "A제조업체"
  }
}

// localStorage 저장 (최소 정보)
localStorage.setItem('user', JSON.stringify({
  ...user,
  menu: transformedMenus,
  tenant_id: tenant.id
}));
```

**2단계: 페이지 접근 시 구성 정보 로드 (캐싱)**
```typescript
// /master-data/item-master-data-management 접근 시
const usePageConfig = (pageType: string) => {
  const [config, setConfig] = useState(null);

  useEffect(() => {
    // 1. 메모리 캐시 확인
    if (pageConfigStore[pageType]) {
      setConfig(pageConfigStore[pageType]);
      return;
    }

    // 2. sessionStorage 캐시 확인
    const cached = sessionStorage.getItem(`page_config_${pageType}`);
    if (cached) {
      const data = JSON.parse(cached);
      setConfig(data);
      pageConfigStore[pageType] = data;
      return;
    }

    // 3. API 요청 (tenant_id 자동 포함)
    fetch(`/api/master-data/pages/${pageType}`)
      .then(res => res.json())
      .then(data => {
        setConfig(data);
        // 캐시 저장
        pageConfigStore[pageType] = data;
        sessionStorage.setItem(`page_config_${pageType}`, JSON.stringify(data));
      });
  }, [pageType]);

  return config;
};
```

**3단계: 구성 변경 시 캐시 무효화**
```typescript
const updatePageConfig = async (pageId: string, updates: any) => {
  await api.put(`/api/master-data/pages/${pageId}`, updates);

  // 캐시 무효화
  delete pageConfigStore[pageType];
  sessionStorage.removeItem(`page_config_${pageType}`);

  // 재로드
  const freshConfig = await api.get(`/api/master-data/pages/${pageType}`);
  setConfig(freshConfig);
};
```

---

### 7.3 Zustand Store 구현

```typescript
// src/stores/pageConfigStore.ts
import { create } from 'zustand';

interface PageConfig {
  itemType: string;
  sections: ItemSection[];
  fields: ItemField[];
  validationRules: any[];
}

interface PageConfigStore {
  // 메모리 캐시
  configs: Record<string, PageConfig>;

  // 구성 로드
  fetchConfig: (pageType: string) => Promise<PageConfig>;

  // 캐시 무효화
  invalidateConfig: (pageType: string) => void;

  // 구성 업데이트
  updateConfig: (pageType: string, updates: Partial<PageConfig>) => Promise<void>;
}

export const usePageConfigStore = create<PageConfigStore>((set, get) => ({
  configs: {},

  fetchConfig: async (pageType: string) => {
    const { configs } = get();

    // 1. 메모리 캐시 확인
    if (configs[pageType]) {
      console.log(`✅ Cache hit (memory): ${pageType}`);
      return configs[pageType];
    }

    // 2. sessionStorage 확인
    const cached = sessionStorage.getItem(`page_config_${pageType}`);
    if (cached) {
      console.log(`✅ Cache hit (session): ${pageType}`);
      const config = JSON.parse(cached);
      set({ configs: { ...configs, [pageType]: config } });
      return config;
    }

    // 3. API 요청 (tenant_id는 쿠키/헤더에서 자동)
    console.log(`🌐 API request: ${pageType}`);
    const response = await fetch(`/api/master-data/pages/${pageType}`);
    const config = await response.json();

    // 캐시 저장
    set({ configs: { ...configs, [pageType]: config } });
    sessionStorage.setItem(`page_config_${pageType}`, JSON.stringify(config));

    return config;
  },

  invalidateConfig: (pageType: string) => {
    const { configs } = get();
    const newConfigs = { ...configs };
    delete newConfigs[pageType];

    set({ configs: newConfigs });
    sessionStorage.removeItem(`page_config_${pageType}`);

    console.log(`🗑️ Cache invalidated: ${pageType}`);
  },

  updateConfig: async (pageType: string, updates: Partial<PageConfig>) => {
    // API 업데이트
    await fetch(`/api/master-data/pages/${pageType}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates)
    });

    // 캐시 무효화 후 재로드
    get().invalidateConfig(pageType);
    await get().fetchConfig(pageType);
  }
}));
```

---

### 7.4 API 설계 (멀티테넌시 지원)

#### 테넌트 컨텍스트 자동 포함

```typescript
// 백엔드: Laravel Middleware
class TenantContextMiddleware {
  public function handle(Request $request, Closure $next) {
    // JWT 토큰 또는 세션에서 tenant_id 추출
    $tenantId = $request->user()->tenant_id;

    // Request에 tenant_id 주입
    $request->merge(['tenant_id' => $tenantId]);

    return $next($request);
  }
}

// 모든 API 쿼리에 자동 적용
ItemPage::where('tenant_id', $tenantId)->get();
```

#### API 엔드포인트

```typescript
// 1. 페이지 구성 조회 (tenant_id 자동 필터링)
GET /api/master-data/pages/{itemType}
Response: {
  success: true,
  data: {
    id: "PAGE-FG-001",
    itemType: "FG",
    tenant_id: "tenant_001",  // 자동 포함
    sections: [...],
    fields: [...]
  }
}

// 2. 구성 저장 (tenant_id 자동 설정)
PUT /api/master-data/pages/{pageId}
Request: {
  sections: [...],
  revisionReason: "단위 옵션 추가"
  // tenant_id는 백엔드에서 자동 설정
}
Response: {
  success: true,
  data: {
    ...updatedPage,
    tenant_id: "tenant_001"  // 자동 설정됨
  }
}

// 3. 테넌트별 옵션 조회
GET /api/master-data/options/units
Response: {
  success: true,
  data: ["EA", "SET", "KG"]  // tenant_001 전용 단위
}
```

---

### 7.5 데이터베이스 스키마 (멀티테넌시)

```sql
-- 모든 마스터 테이블에 tenant_id 추가
ALTER TABLE item_pages
ADD COLUMN tenant_id VARCHAR(255) NOT NULL,
ADD INDEX idx_tenant_pages (tenant_id, item_type);

ALTER TABLE item_sections
ADD COLUMN tenant_id VARCHAR(255) NOT NULL,
ADD INDEX idx_tenant_sections (tenant_id);

ALTER TABLE item_master_fields
ADD COLUMN tenant_id VARCHAR(255) NOT NULL,
ADD INDEX idx_tenant_fields (tenant_id);

ALTER TABLE master_units
ADD COLUMN tenant_id VARCHAR(255) NOT NULL,
ADD INDEX idx_tenant_units (tenant_id);

ALTER TABLE master_materials
ADD COLUMN tenant_id VARCHAR(255) NOT NULL,
ADD INDEX idx_tenant_materials (tenant_id);

-- 복합 유니크 제약 (테넌트별 중복 방지)
ALTER TABLE item_pages
ADD UNIQUE KEY uk_tenant_page (tenant_id, page_name, item_type);
```

---

### 7.6 성능 최적화

#### 캐싱 계층

```
┌──────────────────────────────────────────────────────────┐
│  레벨 1: 메모리 캐시 (Zustand)                            │
│  - 수명: 페이지 새로고침 전까지                           │
│  - 속도: 즉시                                            │
└──────────────────────────────────────────────────────────┘
         ↓ (캐시 미스)
┌──────────────────────────────────────────────────────────┐
│  레벨 2: sessionStorage                                   │
│  - 수명: 브라우저 탭 닫기 전까지                          │
│  - 속도: ~1ms                                            │
└──────────────────────────────────────────────────────────┘
         ↓ (캐시 미스)
┌──────────────────────────────────────────────────────────┐
│  레벨 3: API 요청 (백엔드 캐싱)                           │
│  - 수명: Redis 캐시 (10분)                               │
│  - 속도: ~50-200ms                                       │
└──────────────────────────────────────────────────────────┘
         ↓ (캐시 미스)
┌──────────────────────────────────────────────────────────┐
│  레벨 4: 데이터베이스                                     │
│  - 수명: 영구                                            │
│  - 속도: ~100-500ms                                      │
└──────────────────────────────────────────────────────────┘
```

#### 백엔드 캐싱 (Laravel)

```php
// Redis 캐시 활용
class PageConfigController {
  public function show($itemType) {
    $tenantId = auth()->user()->tenant_id;
    $cacheKey = "page_config:{$tenantId}:{$itemType}";

    return Cache::remember($cacheKey, 600, function() use ($tenantId, $itemType) {
      return ItemPage::where('tenant_id', $tenantId)
                     ->where('item_type', $itemType)
                     ->with(['sections', 'fields'])
                     ->first();
    });
  }

  public function update($pageId, Request $request) {
    $page = ItemPage::findOrFail($pageId);
    $page->update($request->all());

    // 캐시 무효화
    $cacheKey = "page_config:{$page->tenant_id}:{$page->item_type}";
    Cache::forget($cacheKey);

    return $page;
  }
}
```

---

### 7.7 보안 고려사항

#### 테넌트 격리 (Tenant Isolation)

```typescript
// 1. 미들웨어에서 tenant_id 검증
if (page.tenant_id !== user.tenant_id) {
  throw new ForbiddenException('다른 테넌트의 데이터에 접근할 수 없습니다');
}

// 2. 모든 쿼리에 tenant_id 필터 강제
ItemPage::where('tenant_id', auth()->user()->tenant_id)->get();

// 3. Global Scope 적용 (Laravel)
class ItemPage extends Model {
  protected static function booted() {
    static::addGlobalScope('tenant', function (Builder $builder) {
      $builder->where('tenant_id', auth()->user()->tenant_id);
    });
  }
}
```

---

### 7.8 하이브리드 로딩 흐름도

```
사용자 로그인
     ↓
┌─────────────────────────────────────┐
│  기본 정보 로드 (Eager)              │
│  - user { tenant_id }               │
│  - menu 구조                        │
│  - roles                            │
└─────────────────────────────────────┘
     ↓
대시보드 접속 (메뉴만 표시)
     ↓
품목기준관리 페이지 클릭
     ↓
┌─────────────────────────────────────┐
│  페이지 구성 로드 (Lazy + Cache)     │
│  1. 메모리 캐시 확인 → 없음          │
│  2. sessionStorage 확인 → 없음      │
│  3. API 요청 (tenant_id 자동)       │
│  4. 캐시 저장                        │
└─────────────────────────────────────┘
     ↓
페이지 렌더링 (동적 구성)
     ↓
사용자가 구성 변경
     ↓
┌─────────────────────────────────────┐
│  구성 업데이트                       │
│  1. API 업데이트                     │
│  2. 캐시 무효화                      │
│  3. 재로드                           │
└─────────────────────────────────────┘
```

---

## 8. Next.js 15 마이그레이션 계획

### 8.1 디렉토리 구조

```
src/
├── app/
│   └── [locale]/
│       └── (protected)/
│           └── master-data/                   # 품목기준관리
│               ├── page.tsx                   # 메인 페이지 (Server Component)
│               └── layout.tsx                 # 레이아웃
│
├── components/
│   ├── master-data/                           # 품목기준관리 컴포넌트
│   │   ├── HierarchyTab.tsx                   # 계층구조 탭 (Client)
│   │   ├── MasterFieldsTab.tsx                # 마스터 항목 탭 (Client)
│   │   ├── UnitsTab.tsx                       # 단위 탭 (Client)
│   │   ├── MaterialsTab.tsx                   # 재질 탭 (Client)
│   │   ├── SurfaceTab.tsx                     # 표면처리 탭 (Client)
│   │   ├── SpecificationsTab.tsx              # 규격 탭 (Client)
│   │   ├── PageManager.tsx                    # 섹션 관리 (Client)
│   │   ├── SectionManager.tsx                 # 하위섹션 관리 (Client)
│   │   ├── FieldManager.tsx                   # 항목 관리 (Client)
│   │   ├── DraggableField.tsx                 # 드래그 앤 드롭 (Client)
│   │   └── dialogs/
│   │       ├── PageDialog.tsx                 # 섹션 추가 다이얼로그
│   │       ├── SectionDialog.tsx              # 하위섹션 추가 다이얼로그
│   │       ├── FieldDialog.tsx                # 항목 추가 다이얼로그
│   │       ├── MasterFieldDialog.tsx          # 마스터 항목 다이얼로그
│   │       └── OptionDialog.tsx               # 옵션 추가 다이얼로그
│   │
│   └── ui/                                    # shadcn/ui 컴포넌트
│       ├── tabs.tsx
│       ├── dialog.tsx
│       ├── switch.tsx
│       └── ...
│
├── stores/
│   └── masterDataStore.ts                     # Zustand - 품목기준관리 상태
│
├── lib/
│   └── api/
│       └── master-data.ts                     # 품목기준관리 API 클라이언트
│
└── types/
    └── master-data.ts                         # 품목기준관리 타입
```

---

### 8.2 타입 정의

#### src/types/master-data.ts

```typescript
// 섹션 (ItemPage)
export interface ItemPage {
  id: string;
  pageName: string;
  itemType: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
  sections: ItemSection[];
  isActive: boolean;
  absolutePath?: string;
  createdAt: string;
  updatedAt?: string;
}

// 하위섹션 (ItemSection)
export interface ItemSection {
  id: string;
  title: string;
  description?: string;
  category?: string[];
  fields: ItemField[];
  type?: 'fields' | 'bom';
  order: number;
  isCollapsible: boolean;
  isCollapsed: boolean;
  createdAt: string;
}

// 항목 (ItemField)
export interface ItemField {
  id: string;
  name: string;
  fieldKey: string;
  property: ItemFieldProperty;
  description?: string;
  displayCondition?: FieldDisplayCondition;
  masterFieldId?: string;
  order?: number;
  createdAt: string;
}

// 입력 속성
export interface ItemFieldProperty {
  inputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
  required: boolean;
  row: number;
  col: number;
  options?: string[];
}

// 조건부 표시
export interface FieldDisplayCondition {
  fieldKey: string;
  expectedValue: string;
}

// 마스터 항목
export interface ItemMasterField {
  id: string;
  name: string;
  fieldKey: string;
  property: ItemFieldProperty;
  category?: string;
  description?: string;
  isActive: boolean;
  createdAt: string;
}

// 옵션
export interface MasterOption {
  id: string;
  value: string;
  label: string;
  isActive: boolean;
}

// 통합 마스터 데이터
export interface MasterData {
  units: MasterOption[];
  materials: MasterOption[];
  surfaceTreatments: MasterOption[];
  pages: ItemPage[];
  masterFields: ItemMasterField[];
}
```

---

### 8.3 API 클라이언트

#### src/lib/api/master-data.ts

```typescript
import type {
  ItemPage,
  ItemSection,
  ItemField,
  ItemMasterField,
  MasterOption,
  MasterData
} from '@/types/master-data';

const API_URL = process.env.NEXT_PUBLIC_API_URL;

// 통합 조회
export async function fetchMasterData(): Promise<MasterData> {
  const response = await fetch(`${API_URL}/api/master-data`, {
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    throw new Error('Failed to fetch master data');
  }

  const data = await response.json();
  return data.data;
}

// 섹션 관리
export async function fetchPages(): Promise<ItemPage[]> {
  const response = await fetch(`${API_URL}/api/master-data/pages`, {
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    throw new Error('Failed to fetch pages');
  }

  const data = await response.json();
  return data.data;
}

export async function createPage(page: Partial<ItemPage>): Promise<ItemPage> {
  const response = await fetch(`${API_URL}/api/master-data/pages`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(page),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create page');
  }

  const data = await response.json();
  return data.data;
}

export async function updatePage(pageId: string, updates: Partial<ItemPage>): Promise<ItemPage> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(updates),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to update page');
  }

  const data = await response.json();
  return data.data;
}

export async function deletePage(pageId: string): Promise<void> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to delete page');
  }
}

// 하위섹션 관리
export async function createSection(pageId: string, section: Partial<ItemSection>): Promise<ItemSection> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}/sections`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(section),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create section');
  }

  const data = await response.json();
  return data.data;
}

export async function updateSection(
  pageId: string,
  sectionId: string,
  updates: Partial<ItemSection>
): Promise<ItemSection> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(updates),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to update section');
  }

  const data = await response.json();
  return data.data;
}

export async function deleteSection(pageId: string, sectionId: string): Promise<void> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to delete section');
  }
}

// 항목 관리
export async function createField(
  pageId: string,
  sectionId: string,
  field: Partial<ItemField>
): Promise<ItemField> {
  const response = await fetch(`${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}/fields`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(field),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create field');
  }

  const data = await response.json();
  return data.data;
}

export async function updateField(
  pageId: string,
  sectionId: string,
  fieldId: string,
  updates: Partial<ItemField>
): Promise<ItemField> {
  const response = await fetch(
    `${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}/fields/${fieldId}`,
    {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`,
      },
      body: JSON.stringify(updates),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to update field');
  }

  const data = await response.json();
  return data.data;
}

export async function reorderFields(
  pageId: string,
  sectionId: string,
  fieldIds: string[]
): Promise<void> {
  const response = await fetch(
    `${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}/fields/reorder`,
    {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`,
      },
      body: JSON.stringify({ fieldIds }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to reorder fields');
  }
}

export async function deleteField(pageId: string, sectionId: string, fieldId: string): Promise<void> {
  const response = await fetch(
    `${API_URL}/api/master-data/pages/${pageId}/sections/${sectionId}/fields/${fieldId}`,
    {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${getToken()}`,
      },
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to delete field');
  }
}

// 마스터 항목 관리
export async function fetchMasterFields(category?: string): Promise<ItemMasterField[]> {
  const url = category
    ? `${API_URL}/api/master-data/fields?category=${category}`
    : `${API_URL}/api/master-data/fields`;

  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    throw new Error('Failed to fetch master fields');
  }

  const data = await response.json();
  return data.data;
}

export async function createMasterField(field: Partial<ItemMasterField>): Promise<ItemMasterField> {
  const response = await fetch(`${API_URL}/api/master-data/fields`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(field),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create master field');
  }

  const data = await response.json();
  return data.data;
}

export async function updateMasterField(
  fieldId: string,
  updates: Partial<ItemMasterField>
): Promise<ItemMasterField> {
  const response = await fetch(`${API_URL}/api/master-data/fields/${fieldId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(updates),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to update master field');
  }

  const data = await response.json();
  return data.data;
}

export async function deleteMasterField(fieldId: string): Promise<void> {
  const response = await fetch(`${API_URL}/api/master-data/fields/${fieldId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to delete master field');
  }
}

// 옵션 관리 (단위, 재질, 표면처리)
export async function fetchUnits(): Promise<MasterOption[]> {
  const response = await fetch(`${API_URL}/api/master-data/units`, {
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    throw new Error('Failed to fetch units');
  }

  const data = await response.json();
  return data.data;
}

export async function createUnit(option: Partial<MasterOption>): Promise<MasterOption> {
  const response = await fetch(`${API_URL}/api/master-data/units`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`,
    },
    body: JSON.stringify(option),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to create unit');
  }

  const data = await response.json();
  return data.data;
}

export async function deleteUnit(unitId: string): Promise<void> {
  const response = await fetch(`${API_URL}/api/master-data/units/${unitId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${getToken()}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to delete unit');
  }
}

// 재질, 표면처리도 동일한 패턴으로 구현...

// Helper function to get token from cookies
function getToken(): string {
  // Implement token retrieval from cookies
  return '';
}
```

---

### 8.4 Zustand Store

#### src/stores/masterDataStore.ts

```typescript
import { create } from 'zustand';
import type {
  ItemPage,
  ItemMasterField,
  MasterOption,
  MasterData
} from '@/types/master-data';

interface MasterDataStore {
  // State
  pages: ItemPage[];
  masterFields: ItemMasterField[];
  units: MasterOption[];
  materials: MasterOption[];
  surfaceTreatments: MasterOption[];
  isLoading: boolean;
  error: string | null;

  // Actions
  setMasterData: (data: MasterData) => void;
  setPages: (pages: ItemPage[]) => void;
  addPage: (page: ItemPage) => void;
  updatePage: (pageId: string, updates: Partial<ItemPage>) => void;
  deletePage: (pageId: string) => void;

  setMasterFields: (fields: ItemMasterField[]) => void;
  addMasterField: (field: ItemMasterField) => void;
  updateMasterField: (fieldId: string, updates: Partial<ItemMasterField>) => void;
  deleteMasterField: (fieldId: string) => void;

  setUnits: (units: MasterOption[]) => void;
  addUnit: (unit: MasterOption) => void;
  deleteUnit: (unitId: string) => void;

  setLoading: (isLoading: boolean) => void;
  setError: (error: string | null) => void;
}

export const useMasterDataStore = create<MasterDataStore>((set, get) => ({
  // Initial state
  pages: [],
  masterFields: [],
  units: [],
  materials: [],
  surfaceTreatments: [],
  isLoading: false,
  error: null,

  // Actions
  setMasterData: (data) => set({
    pages: data.pages,
    masterFields: data.masterFields,
    units: data.units,
    materials: data.materials,
    surfaceTreatments: data.surfaceTreatments,
  }),

  setPages: (pages) => set({ pages }),

  addPage: (page) => set((state) => ({
    pages: [...state.pages, page],
  })),

  updatePage: (pageId, updates) => set((state) => ({
    pages: state.pages.map((page) =>
      page.id === pageId ? { ...page, ...updates } : page
    ),
  })),

  deletePage: (pageId) => set((state) => ({
    pages: state.pages.filter((page) => page.id !== pageId),
  })),

  setMasterFields: (fields) => set({ masterFields: fields }),

  addMasterField: (field) => set((state) => ({
    masterFields: [...state.masterFields, field],
  })),

  updateMasterField: (fieldId, updates) => set((state) => ({
    masterFields: state.masterFields.map((field) =>
      field.id === fieldId ? { ...field, ...updates } : field
    ),
  })),

  deleteMasterField: (fieldId) => set((state) => ({
    masterFields: state.masterFields.filter((field) => field.id !== fieldId),
  })),

  setUnits: (units) => set({ units }),

  addUnit: (unit) => set((state) => ({
    units: [...state.units, unit],
  })),

  deleteUnit: (unitId) => set((state) => ({
    units: state.units.filter((unit) => unit.id !== unitId),
  })),

  setLoading: (isLoading) => set({ isLoading }),

  setError: (error) => set({ error }),
}));
```

---

### 8.5 메인 페이지

#### src/app/[locale]/(protected)/master-data/page.tsx

```typescript
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import MasterDataClient from '@/components/master-data/MasterDataClient';

export async function generateMetadata(): Promise<Metadata> {
  const t = await getTranslations('MasterData');

  return {
    title: t('title'), // "품목기준관리"
    description: t('description'), // "품목관리에서 사용되는 기준 정보를 설정하고 관리합니다"
  };
}

export default function MasterDataPage() {
  return <MasterDataClient />;
}
```

#### src/components/master-data/MasterDataClient.tsx

```typescript
'use client'

import { useEffect, useState } from 'react';
import { Database } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageHeader } from '@/components/layout/PageHeader';
import HierarchyTab from './HierarchyTab';
import MasterFieldsTab from './MasterFieldsTab';
import UnitsTab from './UnitsTab';
import MaterialsTab from './MaterialsTab';
import SurfaceTab from './SurfaceTab';
import SpecificationsTab from './SpecificationsTab';
import { useMasterDataStore } from '@/stores/masterDataStore';
import { fetchMasterData } from '@/lib/api/master-data';

export default function MasterDataClient() {
  const [activeTab, setActiveTab] = useState('hierarchy');
  const { setMasterData, setLoading, setError } = useMasterDataStore();

  useEffect(() => {
    async function loadData() {
      setLoading(true);
      try {
        const data = await fetchMasterData();
        setMasterData(data);
      } catch (error: any) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    loadData();
  }, []);

  return (
    <div>
      <PageHeader
        title="품목기준관리"
        description="품목관리에서 사용되는 기준 정보를 설정하고 관리합니다"
        icon={Database}
      />

      <Tabs value={activeTab} onValueChange={setActiveTab}>
        <TabsList className="grid w-full grid-cols-6">
          <TabsTrigger value="hierarchy">계층구조</TabsTrigger>
          <TabsTrigger value="items">항목</TabsTrigger>
          <TabsTrigger value="units">단위</TabsTrigger>
          <TabsTrigger value="materials">재질</TabsTrigger>
          <TabsTrigger value="surface">표면처리</TabsTrigger>
          <TabsTrigger value="specifications">규격</TabsTrigger>
        </TabsList>

        <TabsContent value="hierarchy">
          <HierarchyTab />
        </TabsContent>

        <TabsContent value="items">
          <MasterFieldsTab />
        </TabsContent>

        <TabsContent value="units">
          <UnitsTab />
        </TabsContent>

        <TabsContent value="materials">
          <MaterialsTab />
        </TabsContent>

        <TabsContent value="surface">
          <SurfaceTab />
        </TabsContent>

        <TabsContent value="specifications">
          <SpecificationsTab />
        </TabsContent>
      </Tabs>
    </div>
  );
}
```

---

## 9. 구현 우선순위

### Phase 1: 기반 구축 (1주)
```
✅ 타입 정의 (types/master-data.ts)
✅ API 클라이언트 (lib/api/master-data.ts)
✅ Zustand Store (stores/masterDataStore.ts)
✅ 메인 페이지 구조 (app/[locale]/(protected)/master-data/page.tsx)
```

### Phase 2: 단순 탭 구현 (1주)
```
✅ 단위 탭 (UnitsTab.tsx)
✅ 재질 탭 (MaterialsTab.tsx)
✅ 표면처리 탭 (SurfaceTab.tsx)
✅ 옵션 추가 다이얼로그 (dialogs/OptionDialog.tsx)
```

### Phase 3: 마스터 항목 탭 (1주)
```
✅ 마스터 항목 탭 (MasterFieldsTab.tsx)
✅ 품목 분류별 필터링
✅ 마스터 항목 다이얼로그 (dialogs/MasterFieldDialog.tsx)
✅ CRUD 기능 구현
```

### Phase 4: 계층구조 탭 (2주)
```
✅ 섹션 관리 (PageManager.tsx)
✅ 하위섹션 관리 (SectionManager.tsx)
✅ 항목 관리 (FieldManager.tsx)
✅ 드래그 앤 드롭 (DraggableField.tsx)
✅ 조건부 표시 로직
✅ 마스터 항목 연동
```

### Phase 5: Laravel API 연동 (1주)
```
✅ 백엔드 API 구현 협의
✅ 데이터베이스 스키마 확정
✅ API 엔드포인트 개발
✅ 프론트엔드 연동 테스트
```

### Phase 6: 품목등록 화면 동적화 (선택적, 2주)
```
⏳ ItemForm.tsx를 동적 템플릿으로 전환
⏳ MasterData 조회 및 렌더링 로직
⏳ 조건부 표시 적용
⏳ 통합 테스트
```

**총 예상 소요 기간**: 6-8주

---

## 10. 다음 단계

### 10.1 즉시 시작 가능한 작업

**1. 타입 정의 작성**
```bash
src/types/master-data.ts 작성
- ItemPage, ItemSection, ItemField
- ItemMasterField
- MasterOption
- MasterData
```

**2. API 클라이언트 작성**
```bash
src/lib/api/master-data.ts 작성
- fetchMasterData()
- Page CRUD functions
- Section CRUD functions
- Field CRUD functions
- MasterField CRUD functions
- Option CRUD functions
```

**3. Zustand Store 작성**
```bash
src/stores/masterDataStore.ts 작성
- 기본 상태 정의
- CRUD 액션 구현
```

**4. Laravel API 스펙 확정**
```bash
- 백엔드 팀과 API 요구사항 논의
- 데이터베이스 스키마 설계
- API 엔드포인트 목록 확정
```

### 10.2 백엔드 협의 사항

**데이터베이스 테이블 제안**:
```sql
-- 섹션 (item_pages)
CREATE TABLE item_pages (
  id VARCHAR(50) PRIMARY KEY,
  page_name VARCHAR(100) NOT NULL,
  item_type ENUM('FG', 'PT', 'SM', 'RM', 'CS') NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  absolute_path VARCHAR(255),
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);

-- 하위섹션 (item_sections)
CREATE TABLE item_sections (
  id VARCHAR(50) PRIMARY KEY,
  page_id VARCHAR(50) NOT NULL,
  title VARCHAR(100) NOT NULL,
  description TEXT,
  `order` INT NOT NULL,
  is_collapsible BOOLEAN DEFAULT TRUE,
  is_collapsed BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP,
  FOREIGN KEY (page_id) REFERENCES item_pages(id) ON DELETE CASCADE
);

-- 항목 (item_fields)
CREATE TABLE item_fields (
  id VARCHAR(50) PRIMARY KEY,
  section_id VARCHAR(50) NOT NULL,
  name VARCHAR(100) NOT NULL,
  field_key VARCHAR(100) NOT NULL,
  input_type ENUM('textbox', 'dropdown', 'checkbox', 'number', 'date', 'textarea') NOT NULL,
  required BOOLEAN DEFAULT FALSE,
  row_position INT DEFAULT 1,
  col_position INT DEFAULT 1,
  options JSON,
  description TEXT,
  display_condition JSON,
  master_field_id VARCHAR(50),
  `order` INT,
  created_at TIMESTAMP,
  FOREIGN KEY (section_id) REFERENCES item_sections(id) ON DELETE CASCADE,
  FOREIGN KEY (master_field_id) REFERENCES item_master_fields(id) ON DELETE SET NULL
);

-- 마스터 항목 (item_master_fields)
CREATE TABLE item_master_fields (
  id VARCHAR(50) PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  field_key VARCHAR(100) NOT NULL,
  input_type ENUM('textbox', 'dropdown', 'checkbox', 'number', 'date', 'textarea') NOT NULL,
  required BOOLEAN DEFAULT FALSE,
  row_position INT DEFAULT 1,
  col_position INT DEFAULT 1,
  options JSON,
  category VARCHAR(50),
  description TEXT,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP
);

-- 단위 (master_units)
CREATE TABLE master_units (
  id VARCHAR(50) PRIMARY KEY,
  value VARCHAR(50) NOT NULL UNIQUE,
  label VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT TRUE
);

-- 재질 (master_materials)
CREATE TABLE master_materials (
  id VARCHAR(50) PRIMARY KEY,
  value VARCHAR(50) NOT NULL UNIQUE,
  label VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT TRUE
);

-- 표면처리 (master_surface_treatments)
CREATE TABLE master_surface_treatments (
  id VARCHAR(50) PRIMARY KEY,
  value VARCHAR(50) NOT NULL UNIQUE,
  label VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT TRUE
);
```

**API 인증 방식**:
- Laravel Sanctum (쿠키 기반)
- CSRF 토큰 검증

**CORS 설정**:
- Next.js 프론트엔드 도메인 허용
- Credentials 포함 요청 허용

---

## 부록

### A. React 프로젝트 참조 파일

**주요 파일**:
- `ItemMasterDataManagement.tsx` (1,413줄) - 메인 컴포넌트
- `DataContext.tsx` (6,697줄) - 전역 상태 관리
- `SpecificationManagement.tsx` - 규격 관리
- `DraggableField.tsx` - 드래그 앤 드롭

**로컬스토리지 키**:
- `unit-options`
- `material-options`
- `surface-treatment-options`
- `item-classifications`

### B. 용어 정의

| 용어 | 설명 |
|-----|------|
| 섹션 (ItemPage) | 품목 유형별 페이지 (예: 제품(FG) 등록) |
| 하위섹션 (ItemSection) | 섹션 내 그룹 (예: 기본정보, BOM) |
| 항목 (ItemField) | 입력 필드 (예: 품목코드, 단위) |
| 마스터 항목 (ItemMasterField) | 재사용 가능한 항목 템플릿 |
| 옵션 (MasterOption) | 드롭다운 선택지 (단위, 재질, 표면처리) |
| 조건부 표시 (FieldDisplayCondition) | 특정 값일 때만 항목 표시 |

### C. 다음 세션 시작 시 전달 내용

> "품목기준관리 시스템 구현을 시작하려고 합니다. `[ANALYSIS] item-master-data-management.md` 문서를 참조해주세요."

---

**문서 끝**