# 본서버 → 개발서버 마이그레이션 가이드

## 개요

본 문서는 **본서버(Production)**의 `screen_layouts` (V1) 데이터를 **개발서버(Development)**의 `screen_layouts_v2` 시스템으로 마이그레이션하는 절차를 정의합니다.

### 마이그레이션 방향
```
본서버 (Production)          개발서버 (Development)
┌─────────────────────┐      ┌─────────────────────┐
│ screen_layouts (V1) │  →   │ screen_layouts_v2   │
│ - 컴포넌트별 레코드  │      │ - 화면당 1개 레코드  │
│ - properties JSONB  │      │ - layout_data JSONB │
└─────────────────────┘      └─────────────────────┘
```

### 최종 목표
개발서버에서 완성 후 **개발서버 → 본서버**로 배포

---

## 1. V1 vs V2 구조 차이

### 1.1 screen_layouts (V1) - 본서버

```sql
-- 컴포넌트별 1개 레코드
CREATE TABLE screen_layouts (
  layout_id SERIAL PRIMARY KEY,
  screen_id INTEGER,
  component_type VARCHAR(50),
  component_id VARCHAR(100),
  properties JSONB,  -- 모든 설정값 포함
  ...
);
```

**특징:**
- 화면당 N개 레코드 (컴포넌트 수만큼)
- `properties`에 모든 설정 저장 (defaults + overrides 구분 없음)
- `menu_objid` 기반 채번/카테고리 관리

### 1.2 screen_layouts_v2 - 개발서버

```sql
-- 화면당 1개 레코드
CREATE TABLE screen_layouts_v2 (
  layout_id SERIAL PRIMARY KEY,
  screen_id INTEGER NOT NULL,
  company_code VARCHAR(20) NOT NULL,
  layout_data JSONB NOT NULL DEFAULT '{}'::jsonb,
  UNIQUE(screen_id, company_code)
);
```

**layout_data 구조:**
```json
{
  "version": "2.0",
  "components": [
    {
      "id": "comp_xxx",
      "url": "@/lib/registry/components/v2-table-list",
      "position": { "x": 0, "y": 0 },
      "size": { "width": 100, "height": 50 },
      "displayOrder": 0,
      "overrides": {
        "tableName": "inspection_standard",
        "columns": ["id", "name"]
      }
    }
  ],
  "updatedAt": "2026-02-03T12:00:00Z"
}
```

**특징:**
- 화면당 1개 레코드
- `url` + `overrides` 방식 (Zod 스키마 defaults와 병합)
- `table_name + column_name` 기반 채번/카테고리 관리 (전역)

---

## 2. 데이터 타입 관리 구조 (V2)

### 2.1 핵심 테이블 관계

```
table_type_columns (컬럼 타입 정의)
├── input_type = 'category' → category_values
├── input_type = 'numbering' → numbering_rules
└── input_type = 'text', 'date', 'number', etc.
```

### 2.2 table_type_columns

각 테이블의 컬럼별 입력 타입을 정의합니다.

```sql
SELECT table_name, column_name, input_type, column_label
FROM table_type_columns
WHERE input_type IN ('category', 'numbering');
```

**주요 input_type:**
| input_type | 설명 | 연결 테이블 |
|------------|------|-------------|
| text | 텍스트 입력 | - |
| number | 숫자 입력 | - |
| date | 날짜 입력 | - |
| category | 카테고리 드롭다운 | category_values |
| numbering | 자동 채번 | numbering_rules |
| entity | 엔티티 검색 | - |

### 2.3 category_values (카테고리 관리)

```sql
-- 카테고리 값 조회
SELECT value_id, table_name, column_name, value_code, value_label, 
       parent_value_id, depth, company_code
FROM category_values
WHERE table_name = 'inspection_standard'
  AND column_name = 'inspection_method'
  AND company_code = 'COMPANY_7';
```

**V1 vs V2 차이:**
| 구분 | V1 | V2 |
|------|----|----|
| 키 | menu_objid | table_name + column_name |
| 범위 | 화면별 | 전역 (테이블.컬럼별) |
| 계층 | 단일 | 3단계 (대/중/소분류) |

### 2.4 numbering_rules (채번 규칙)

```sql
-- 채번 규칙 조회
SELECT rule_id, rule_name, table_name, column_name, separator, 
       reset_period, current_sequence, company_code
FROM numbering_rules
WHERE company_code = 'COMPANY_7';
```

**연결 방식:**
```
table_type_columns.detail_settings = '{"numberingRuleId": "rule-xxx"}'
                                              ↓
                               numbering_rules.rule_id = "rule-xxx"
```

---

## 3. 컴포넌트 매핑

### 3.1 기본 컴포넌트 매핑

| V1 (본서버) | V2 (개발서버) | 비고 |
|-------------|---------------|------|
| table-list | v2-table-list | 테이블 목록 |
| button-primary | v2-button-primary | 버튼 |
| text-input | v2-text-input | 텍스트 입력 |
| select-basic | v2-select | 드롭다운 |
| date-input | v2-date-input | 날짜 입력 |
| entity-search-input | v2-entity-search | 엔티티 검색 |
| tabs-widget | v2-tabs-widget | 탭 |

### 3.2 특수 컴포넌트 매핑

| V1 (본서버) | V2 (개발서버) | 마이그레이션 방식 |
|-------------|---------------|-------------------|
| category-manager | v2-category-manager | table_name 기반으로 변경 |
| numbering-rule | v2-numbering-rule | table_name 기반으로 변경 |
| 모달 화면 | overlay 통합 | 부모 화면에 통합 |

### 3.3 모달 처리 방식 변경

**V1 (본서버):**
```
화면 A (screen_id: 142) - 검사장비관리
    └── 버튼 클릭 → 화면 B (screen_id: 143) - 검사장비 등록모달
```

**V2 (개발서버):**
```
화면 A (screen_id: 142) - 검사장비관리
    └── v2-dialog-form 컴포넌트로 모달 통합
```

---

## 4. 마이그레이션 절차

### 4.1 사전 분석

```sql
-- 1. 본서버 화면 목록 확인
SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name,
       COUNT(sl.layout_id) as component_count
FROM screen_definitions sd
LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE sd.screen_code LIKE 'COMPANY_7_%'
  AND sd.screen_name LIKE '%품질%'
GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name;

-- 2. 개발서버 V2 화면 현황 확인
SELECT sd.screen_id, sd.screen_code, sd.screen_name,
       sv2.layout_data IS NOT NULL as has_v2_layout
FROM screen_definitions sd
LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id
WHERE sd.company_code = 'COMPANY_7';
```

### 4.2 Step 1: screen_definitions 동기화

```sql
-- 본서버에만 있는 화면을 개발서버에 추가
INSERT INTO screen_definitions (screen_code, screen_name, table_name, company_code, ...)
SELECT screen_code, screen_name, table_name, company_code, ...
FROM [본서버].screen_definitions
WHERE screen_code NOT IN (SELECT screen_code FROM screen_definitions);
```

### 4.3 Step 2: V1 → V2 레이아웃 변환

```typescript
// 변환 로직 (pseudo-code)
async function convertV1toV2(screenId: number, companyCode: string) {
  // 1. V1 레이아웃 조회
  const v1Layouts = await getV1Layouts(screenId);
  
  // 2. V2 형식으로 변환
  const v2Layout = {
    version: "2.0",
    components: v1Layouts.map(v1 => ({
      id: v1.component_id,
      url: mapComponentUrl(v1.component_type),
      position: { x: v1.position_x, y: v1.position_y },
      size: { width: v1.width, height: v1.height },
      displayOrder: v1.display_order,
      overrides: extractOverrides(v1.properties)
    })),
    updatedAt: new Date().toISOString()
  };
  
  // 3. V2 테이블에 저장
  await saveV2Layout(screenId, companyCode, v2Layout);
}

function mapComponentUrl(v1Type: string): string {
  const mapping = {
    'table-list': '@/lib/registry/components/v2-table-list',
    'button-primary': '@/lib/registry/components/v2-button-primary',
    'category-manager': '@/lib/registry/components/v2-category-manager',
    'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
    // ... 기타 매핑
  };
  return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
}
```

### 4.4 Step 3: 카테고리 데이터 마이그레이션

```sql
-- 본서버 카테고리 데이터 → 개발서버 category_values
INSERT INTO category_values (
  table_name, column_name, value_code, value_label, 
  value_order, parent_value_id, depth, company_code
)
SELECT 
  -- V1 카테고리 데이터를 table_name + column_name 기반으로 변환
  'inspection_standard' as table_name,
  'inspection_method' as column_name,
  value_code,
  value_label,
  sort_order,
  NULL as parent_value_id,
  1 as depth,
  'COMPANY_7' as company_code
FROM [본서버_카테고리_데이터];
```

### 4.5 Step 4: 채번 규칙 마이그레이션

```sql
-- 본서버 채번 규칙 → 개발서버 numbering_rules
INSERT INTO numbering_rules (
  rule_id, rule_name, table_name, column_name,
  separator, reset_period, current_sequence, company_code
)
SELECT 
  rule_id,
  rule_name,
  'inspection_standard' as table_name,
  'inspection_code' as column_name,
  separator,
  reset_period,
  0 as current_sequence,  -- 시퀀스 초기화
  'COMPANY_7' as company_code
FROM [본서버_채번_규칙];
```

### 4.6 Step 5: table_type_columns 설정

```sql
-- 카테고리 컬럼 설정
UPDATE table_type_columns
SET input_type = 'category'
WHERE table_name = 'inspection_standard'
  AND column_name = 'inspection_method'
  AND company_code = 'COMPANY_7';

-- 채번 컬럼 설정
UPDATE table_type_columns
SET 
  input_type = 'numbering',
  detail_settings = '{"numberingRuleId": "rule-xxx"}'
WHERE table_name = 'inspection_standard'
  AND column_name = 'inspection_code'
  AND company_code = 'COMPANY_7';
```

---

## 5. 품질관리 메뉴 마이그레이션 현황

### 5.1 화면 매핑 현황

| 본서버 코드 | 화면명 | 테이블 | 개발서버 상태 | 비고 |
|-------------|--------|--------|---------------|------|
| COMPANY_7_126 | 검사정보 관리 | inspection_standard | ✅ V2 존재 | 컴포넌트 수 확인 필요 |
| COMPANY_7_127 | 품목옵션 설정 | - | ✅ V2 존재 | v2-category-manager 사용중 |
| COMPANY_7_138 | 카테고리 설정 | inspection_standard | ❌ 누락 | V2: table_name 기반으로 변경 |
| COMPANY_7_139 | 코드 설정 | inspection_standard | ❌ 누락 | V2: table_name 기반으로 변경 |
| COMPANY_7_142 | 검사장비 관리 | inspection_equipment_mng | ❌ 누락 | 모달 통합 필요 |
| COMPANY_7_143 | 검사장비 등록모달 | inspection_equipment_mng | ❌ 누락 | COMPANY_7_142에 통합 |
| COMPANY_7_144 | 불량기준 정보 | defect_standard_mng | ❌ 누락 | 모달 통합 필요 |
| COMPANY_7_145 | 불량기준 등록모달 | defect_standard_mng | ❌ 누락 | COMPANY_7_144에 통합 |

### 5.2 카테고리/채번 컬럼 현황

**inspection_standard:**
| 컬럼 | input_type | 라벨 |
|------|------------|------|
| inspection_method | category | 검사방법 |
| unit | category | 단위 |
| apply_type | category | 적용구분 |
| inspection_type | category | 유형 |

**inspection_equipment_mng:**
| 컬럼 | input_type | 라벨 |
|------|------------|------|
| equipment_type | category | 장비유형 |
| installation_location | category | 설치장소 |
| equipment_status | category | 장비상태 |

**defect_standard_mng:**
| 컬럼 | input_type | 라벨 |
|------|------------|------|
| defect_type | category | 불량유형 |
| severity | category | 심각도 |
| inspection_type | category | 검사유형 |

---

## 6. 자동화 스크립트

### 6.1 마이그레이션 실행 스크립트

```typescript
// backend-node/src/scripts/migrateV1toV2.ts
import { getPool } from "../database/db";

interface MigrationResult {
  screenCode: string;
  success: boolean;
  message: string;
  componentCount?: number;
}

async function migrateScreenToV2(
  screenCode: string,
  companyCode: string
): Promise<MigrationResult> {
  const pool = getPool();
  
  try {
    // 1. V1 레이아웃 조회 (본서버에서)
    const v1Result = await pool.query(`
      SELECT sl.*, sd.table_name, sd.screen_name
      FROM screen_layouts sl
      JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
      WHERE sd.screen_code = $1
      ORDER BY sl.display_order
    `, [screenCode]);
    
    if (v1Result.rows.length === 0) {
      return { screenCode, success: false, message: "V1 레이아웃 없음" };
    }
    
    // 2. V2 형식으로 변환
    const components = v1Result.rows
      .filter(row => row.component_type !== '_metadata')
      .map(row => ({
        id: row.component_id || `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
        url: mapComponentUrl(row.component_type),
        position: { x: row.position_x || 0, y: row.position_y || 0 },
        size: { width: row.width || 100, height: row.height || 50 },
        displayOrder: row.display_order || 0,
        overrides: extractOverrides(row.properties, row.component_type)
      }));
    
    const layoutData = {
      version: "2.0",
      components,
      migratedFrom: "V1",
      migratedAt: new Date().toISOString()
    };
    
    // 3. 개발서버 V2 테이블에 저장
    const screenId = v1Result.rows[0].screen_id;
    
    await pool.query(`
      INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data)
      VALUES ($1, $2, $3)
      ON CONFLICT (screen_id, company_code) 
      DO UPDATE SET layout_data = $3, updated_at = NOW()
    `, [screenId, companyCode, JSON.stringify(layoutData)]);
    
    return {
      screenCode,
      success: true,
      message: "마이그레이션 완료",
      componentCount: components.length
    };
  } catch (error: any) {
    return { screenCode, success: false, message: error.message };
  }
}

function mapComponentUrl(v1Type: string): string {
  const mapping: Record<string, string> = {
    'table-list': '@/lib/registry/components/v2-table-list',
    'button-primary': '@/lib/registry/components/v2-button-primary',
    'text-input': '@/lib/registry/components/v2-text-input',
    'select-basic': '@/lib/registry/components/v2-select',
    'date-input': '@/lib/registry/components/v2-date-input',
    'entity-search-input': '@/lib/registry/components/v2-entity-search',
    'category-manager': '@/lib/registry/components/v2-category-manager',
    'numbering-rule': '@/lib/registry/components/v2-numbering-rule',
    'tabs-widget': '@/lib/registry/components/v2-tabs-widget',
    'textarea-basic': '@/lib/registry/components/v2-textarea',
  };
  return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`;
}

function extractOverrides(properties: any, componentType: string): Record<string, any> {
  if (!properties) return {};
  
  // V2 Zod 스키마 defaults와 비교하여 다른 값만 추출
  // (실제 구현 시 각 컴포넌트의 defaultConfig와 비교)
  const overrides: Record<string, any> = {};
  
  // 필수 설정만 추출
  if (properties.tableName) overrides.tableName = properties.tableName;
  if (properties.columns) overrides.columns = properties.columns;
  if (properties.label) overrides.label = properties.label;
  if (properties.onClick) overrides.onClick = properties.onClick;
  
  return overrides;
}
```

---

## 7. 검증 체크리스트

### 7.1 마이그레이션 전

- [ ] 본서버 화면 목록 확인
- [ ] 개발서버 기존 V2 데이터 백업
- [ ] 컴포넌트 매핑 테이블 검토
- [ ] 카테고리/채번 데이터 분석

### 7.2 마이그레이션 후

- [ ] screen_definitions 동기화 확인
- [ ] screen_layouts_v2 데이터 생성 확인
- [ ] 컴포넌트 렌더링 테스트
- [ ] 카테고리 드롭다운 동작 확인
- [ ] 채번 규칙 동작 확인
- [ ] 저장/수정/삭제 기능 테스트

### 7.3 모달 통합 확인

- [ ] 기존 모달 화면 → overlay 통합 완료
- [ ] 부모-자식 데이터 연동 확인
- [ ] 모달 열기/닫기 동작 확인

---

## 8. 롤백 계획

마이그레이션 실패 시 롤백 절차:

```sql
-- 1. V2 레이아웃 롤백
DELETE FROM screen_layouts_v2 
WHERE screen_id IN (
  SELECT screen_id FROM screen_definitions 
  WHERE screen_code LIKE 'COMPANY_7_%'
);

-- 2. 추가된 screen_definitions 롤백
DELETE FROM screen_definitions
WHERE screen_code IN ('신규_추가된_코드들')
  AND company_code = 'COMPANY_7';

-- 3. category_values 롤백
DELETE FROM category_values
WHERE company_code = 'COMPANY_7'
  AND created_at > '[마이그레이션_시작_시간]';

-- 4. numbering_rules 롤백
DELETE FROM numbering_rules
WHERE company_code = 'COMPANY_7'
  AND created_at > '[마이그레이션_시작_시간]';
```

---

## 9. 참고 자료

### 관련 코드 파일

- **V2 Category Manager**: `frontend/lib/registry/components/v2-category-manager/`
- **V2 Numbering Rule**: `frontend/lib/registry/components/v2-numbering-rule/`
- **Category Service**: `backend-node/src/services/categoryTreeService.ts`
- **Numbering Service**: `backend-node/src/services/numberingRuleService.ts`

### 관련 문서

- [V2 컴포넌트 분석 가이드](../V2_컴포넌트_분석_가이드.md)
- [V2 컴포넌트 연동 가이드](../V2_컴포넌트_연동_가이드.md)
- [화면 개발 표준 가이드](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)
- [컴포넌트 레이아웃 V2 아키텍처](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md)

---

## 변경 이력

| 날짜 | 작성자 | 내용 |
|------|--------|------|
| 2026-02-03 | DDD1542 | 초안 작성 |
