# 화면 복제 로직 V2 마이그레이션 계획서

> 작성일: 2026-01-28

## 1. 현황 분석

### 1.1 현재 복제 방식 (Legacy)

```
테이블: screen_layouts (다중 레코드)
방식: 화면당 N개 레코드 (컴포넌트 수만큼)
저장: properties에 전체 설정 "박제"
```

**데이터 구조:**
```sql
-- 화면당 여러 레코드
SELECT * FROM screen_layouts WHERE screen_id = 123;
-- layout_id | screen_id | component_type | component_id | properties (전체 설정)
-- 1         | 123       | table-list     | comp_001     | {"tableName": "user", "columns": [...], ...}
-- 2         | 123       | button         | comp_002     | {"label": "저장", "variant": "default", ...}
```

### 1.2 V2 방식

```
테이블: screen_layouts_v2 (1개 레코드)
방식: 화면당 1개 레코드 (JSONB)
저장: url + overrides (차이값만)
```

**데이터 구조:**
```sql
-- 화면당 1개 레코드
SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = 123;
-- {
--   "version": "2.0",
--   "components": [
--     { "id": "comp_001", "url": "@/lib/registry/components/table-list", "overrides": {...} },
--     { "id": "comp_002", "url": "@/lib/registry/components/button-primary", "overrides": {...} }
--   ]
-- }
```

---

## 2. 현재 복제 로직 분석

### 2.1 복제 진입점 (2곳)

| 경로 | 파일 | 함수 | 용도 |
|-----|------|------|-----|
| 단일 화면 복제 | `screenManagementService.ts` | `copyScreen()` | 화면 관리에서 개별 화면 복제 |
| 메뉴 일괄 복제 | `menuCopyService.ts` | `copyScreens()` | 메뉴 복제 시 연결된 화면들 복제 |

### 2.2 screenManagementService.copyScreen() 흐름

```
1. screen_definitions 조회 (원본)
2. screen_definitions INSERT (대상)
3. screen_layouts 조회 (원본) ← Legacy
4. flowId 수집 및 복제 (회사 간 복제 시)
5. numberingRuleId 수집 및 복제 (회사 간 복제 시)
6. componentId 재생성 (idMapping)
7. properties 내 참조 업데이트 (flowId, ruleId)
8. screen_layouts INSERT (대상) ← Legacy
```

**V2 처리: ❌ 없음**

### 2.3 menuCopyService.copyScreens() 흐름

```
1단계: screen_definitions 처리
  - 기존 복사본 존재 시: 업데이트
  - 없으면: 신규 생성
  - screenIdMap 생성

2단계: screen_layouts 처리
  - 원본 조회
  - componentIdMap 생성
  - properties 내 참조 업데이트 (screenId, flowId, ruleId, menuId)
  - 배치 INSERT
```

**V2 처리: ❌ 없음**

### 2.4 복제 시 처리되는 참조 ID들

| 참조 ID | 설명 | 매핑 방식 |
|--------|-----|----------|
| `componentId` | 컴포넌트 고유 ID | 새로 생성 (`comp_xxx`) |
| `parentId` | 부모 컴포넌트 ID | componentIdMap으로 매핑 |
| `flowId` | 노드 플로우 ID | flowIdMap으로 매핑 (회사 간 복제 시) |
| `numberingRuleId` | 채번 규칙 ID | ruleIdMap으로 매핑 (회사 간 복제 시) |
| `screenId` (탭) | 탭에서 참조하는 화면 ID | screenIdMap으로 매핑 |
| `menuObjid` | 메뉴 ID | menuIdMap으로 매핑 |

---

## 3. V2 마이그레이션 시 변경 필요 사항

### 3.1 핵심 변경점

| 항목 | Legacy | V2 |
|-----|--------|-----|
| 읽기 테이블 | `screen_layouts` | `screen_layouts_v2` |
| 쓰기 테이블 | `screen_layouts` | `screen_layouts_v2` |
| 데이터 형태 | N개 레코드 | 1개 JSONB |
| ID 매핑 위치 | 각 레코드의 컬럼 | JSONB 내부 순회 |
| 참조 업데이트 | `properties` JSON | `overrides` JSON |

### 3.2 수정해야 할 함수들

#### screenManagementService.ts

| 함수 | 변경 내용 |
|-----|----------|
| `copyScreen()` | screen_layouts_v2 복제 로직 추가 |
| `collectFlowIdsFromLayouts()` | V2 JSONB 구조에서 flowId 수집 |
| `collectNumberingRuleIdsFromLayouts()` | V2 JSONB 구조에서 ruleId 수집 |
| `updateFlowIdsInProperties()` | V2 overrides 내 flowId 업데이트 |
| `updateNumberingRuleIdsInProperties()` | V2 overrides 내 ruleId 업데이트 |

#### menuCopyService.ts

| 함수 | 변경 내용 |
|-----|----------|
| `copyScreens()` | screen_layouts_v2 복제 로직 추가 |
| `hasLayoutChanges()` | V2 JSONB 비교 로직 |
| `updateReferencesInProperties()` | V2 overrides 내 참조 업데이트 |

### 3.3 새로 추가할 함수들

```typescript
// V2 레이아웃 복제 (공통)
async copyLayoutV2(
  sourceScreenId: number,
  targetScreenId: number,
  targetCompanyCode: string,
  mappings: {
    componentIdMap: Map<string, string>;
    flowIdMap: Map<number, number>;
    ruleIdMap: Map<string, string>;
    screenIdMap: Map<number, number>;
    menuIdMap?: Map<number, number>;
  },
  client: PoolClient
): Promise<void>

// V2 JSONB에서 참조 ID 수집
collectReferencesFromLayoutV2(layoutData: any): {
  flowIds: Set<number>;
  ruleIds: Set<string>;
  screenIds: Set<number>;
}

// V2 JSONB 내 참조 업데이트
updateReferencesInLayoutV2(
  layoutData: any,
  mappings: { ... }
): any
```

---

## 4. 마이그레이션 전략

### 4.1 전략: V2 완전 전환

```
결정: V2만 복제 (Legacy 복제 제거)
이유: 깔끔한 코드, 유지보수 용이, V2 아키텍처 일관성
전제: 기존 화면들은 이미 screen_layouts_v2로 마이그레이션 완료 (1,347개 100%)
```

### 4.2 단계별 계획

#### Phase 1: V2 복제 로직 구현 및 전환

```
목표: Legacy 복제를 V2 복제로 완전 교체
영향: 복제 시 screen_layouts_v2 테이블만 사용

작업:
1. copyLayoutV2() 공통 함수 구현
2. screenManagementService.copyScreen() - Legacy → V2 교체
3. menuCopyService.copyScreens() - Legacy → V2 교체
4. 테스트 및 검증
```

#### Phase 2: Legacy 코드 정리

```
목표: 불필요한 Legacy 복제 코드 제거
영향: 코드 간소화

작업:
1. screen_layouts 관련 복제 코드 제거
2. 관련 헬퍼 함수 정리 (collectFlowIdsFromLayouts 등)
3. 코드 리뷰 및 정리
```

#### Phase 3: Legacy 테이블 정리 (선택, 추후)

```
목표: 불필요한 테이블 제거
영향: 데이터 정리

작업:
1. screen_layouts 테이블 데이터 백업
2. screen_layouts 테이블 삭제 (또는 보관)
3. 관련 코드 정리
```

---

## 5. 상세 구현 계획

### 5.1 Phase 1 작업 목록

| # | 작업 | 파일 | 예상 공수 |
|---|-----|------|---------|
| 1 | `copyLayoutV2()` 공통 함수 구현 | screenManagementService.ts | 2시간 |
| 2 | `collectReferencesFromLayoutV2()` 구현 | screenManagementService.ts | 1시간 |
| 3 | `updateReferencesInLayoutV2()` 구현 | screenManagementService.ts | 2시간 |
| 4 | `copyScreen()` - Legacy 제거, V2로 교체 | screenManagementService.ts | 2시간 |
| 5 | `copyScreens()` - Legacy 제거, V2로 교체 | menuCopyService.ts | 3시간 |
| 6 | 단위 테스트 | - | 2시간 |
| 7 | 통합 테스트 | - | 2시간 |

**총 예상 공수: 14시간 (약 2일)**

### 5.2 주요 변경 포인트

#### copyScreen() 변경 전후

**Before (Legacy):**
```typescript
// 4. 원본 화면의 레이아웃 정보 조회
const sourceLayoutsResult = await client.query<any>(
  `SELECT * FROM screen_layouts WHERE screen_id = $1`,
  [sourceScreenId]
);
// ... N개 레코드 순회하며 INSERT
```

**After (V2):**
```typescript
// 4. 원본 V2 레이아웃 조회
const sourceLayoutV2 = await client.query(
  `SELECT layout_data FROM screen_layouts_v2 
   WHERE screen_id = $1 AND company_code = $2`,
  [sourceScreenId, sourceCompanyCode]
);
// ... JSONB 변환 후 1개 레코드 INSERT
```

#### copyScreens() 변경 전후

**Before (Legacy):**
```typescript
// 레이아웃 배치 INSERT
await client.query(
  `INSERT INTO screen_layouts (...) VALUES ${layoutValues.join(", ")}`,
  layoutParams
);
```

**After (V2):**
```typescript
// V2 레이아웃 UPSERT
await this.copyLayoutV2(
  originalScreenId, targetScreenId, sourceCompanyCode, targetCompanyCode,
  { componentIdMap, flowIdMap, ruleIdMap, screenIdMap, menuIdMap },
  client
);
```

### 5.2 copyLayoutV2() 구현 방안

```typescript
private async copyLayoutV2(
  sourceScreenId: number,
  targetScreenId: number,
  sourceCompanyCode: string,
  targetCompanyCode: string,
  mappings: {
    componentIdMap: Map<string, string>;
    flowIdMap?: Map<number, number>;
    ruleIdMap?: Map<string, string>;
    screenIdMap?: Map<number, number>;
    menuIdMap?: Map<number, number>;
  },
  client: PoolClient
): Promise<void> {
  // 1. 원본 V2 레이아웃 조회
  const sourceResult = await client.query(
    `SELECT layout_data FROM screen_layouts_v2 
     WHERE screen_id = $1 AND company_code = $2`,
    [sourceScreenId, sourceCompanyCode]
  );

  if (sourceResult.rows.length === 0) {
    // V2 레이아웃 없으면 스킵 (Legacy만 있는 경우)
    return;
  }

  const layoutData = sourceResult.rows[0].layout_data;

  // 2. components 배열 순회하며 ID 매핑
  const updatedComponents = layoutData.components.map((comp: any) => {
    const newId = mappings.componentIdMap.get(comp.id) || comp.id;
    
    // overrides 내 참조 업데이트
    let updatedOverrides = { ...comp.overrides };
    
    // flowId 매핑
    if (mappings.flowIdMap && updatedOverrides.flowId) {
      const newFlowId = mappings.flowIdMap.get(updatedOverrides.flowId);
      if (newFlowId) updatedOverrides.flowId = newFlowId;
    }
    
    // numberingRuleId 매핑
    if (mappings.ruleIdMap && updatedOverrides.numberingRuleId) {
      const newRuleId = mappings.ruleIdMap.get(updatedOverrides.numberingRuleId);
      if (newRuleId) updatedOverrides.numberingRuleId = newRuleId;
    }
    
    // screenId 매핑 (탭 컴포넌트 등)
    if (mappings.screenIdMap && updatedOverrides.screenId) {
      const newScreenId = mappings.screenIdMap.get(updatedOverrides.screenId);
      if (newScreenId) updatedOverrides.screenId = newScreenId;
    }
    
    // tabs 배열 내 screenId 매핑
    if (mappings.screenIdMap && Array.isArray(updatedOverrides.tabs)) {
      updatedOverrides.tabs = updatedOverrides.tabs.map((tab: any) => ({
        ...tab,
        screenId: mappings.screenIdMap.get(tab.screenId) || tab.screenId
      }));
    }

    return {
      ...comp,
      id: newId,
      overrides: updatedOverrides
    };
  });

  const newLayoutData = {
    ...layoutData,
    components: updatedComponents,
    updatedAt: new Date().toISOString()
  };

  // 3. 대상 V2 레이아웃 저장 (UPSERT)
  await client.query(
    `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at)
     VALUES ($1, $2, $3, NOW(), NOW())
     ON CONFLICT (screen_id, company_code) 
     DO UPDATE SET layout_data = $3, updated_at = NOW()`,
    [targetScreenId, targetCompanyCode, JSON.stringify(newLayoutData)]
  );
}
```

---

## 6. 테스트 계획

### 6.1 단위 테스트

| 테스트 케이스 | 설명 |
|-------------|------|
| V2 레이아웃 복제 - 기본 | 단순 컴포넌트 복제 |
| V2 레이아웃 복제 - flowId 매핑 | 회사 간 복제 시 flowId 변경 확인 |
| V2 레이아웃 복제 - ruleId 매핑 | 회사 간 복제 시 ruleId 변경 확인 |
| V2 레이아웃 복제 - 탭 screenId 매핑 | 탭 컴포넌트의 screenId 변경 확인 |
| V2 레이아웃 없는 경우 | Legacy만 있는 화면 복제 시 스킵 확인 |

### 6.2 통합 테스트

| 테스트 케이스 | 설명 |
|-------------|------|
| 단일 화면 복제 (같은 회사) | copyScreen() - 동일 회사 내 복제 |
| 단일 화면 복제 (다른 회사) | copyScreen() - 회사 간 복제 |
| 메뉴 일괄 복제 | copyScreens() - 여러 화면 동시 복제 |
| 모달 포함 복제 | copyScreenWithModals() - 메인 + 모달 복제 |

### 6.3 검증 항목

```
복제 후 확인:
- [ ] screen_layouts_v2에 레코드 생성됨
- [ ] componentId가 새로 생성됨
- [ ] flowId가 정확히 매핑됨
- [ ] numberingRuleId가 정확히 매핑됨
- [ ] 탭 컴포넌트의 screenId가 정확히 매핑됨
- [ ] screen_layouts(Legacy)는 복제되지 않음
- [ ] 복제된 화면이 프론트엔드에서 정상 로드됨
- [ ] 복제된 화면 편집/저장 정상 동작
```

---

## 7. 영향 분석

### 7.1 영향 받는 기능

| 기능 | 영향 | 비고 |
|-----|-----|-----|
| 화면 관리 - 화면 복제 | 직접 영향 | copyScreen() |
| 화면 관리 - 그룹 복제 | 직접 영향 | copyScreenWithModals() |
| 메뉴 복제 | 직접 영향 | menuCopyService.copyScreens() |
| 화면 디자이너 | 간접 영향 | 복제된 화면 로드 시 V2 사용 |

### 7.2 롤백 계획

```
V2 전환 롤백 (필요시):
1. Git에서 이전 버전 복원 (copyScreen, copyScreens)
2. Legacy 복제 코드 복원
3. 테스트 후 배포

주의사항:
- V2로 복제된 화면들은 screen_layouts_v2에만 데이터 존재
- 롤백 시 해당 화면들은 screen_layouts에 데이터 없음
- 필요시 V2 → Legacy 역변환 스크립트 실행
```

---

## 8. 관련 파일

### 8.1 수정 대상

| 파일 | 변경 내용 |
|-----|----------|
| `backend-node/src/services/screenManagementService.ts` | copyLayoutV2(), copyScreen() 수정 |
| `backend-node/src/services/menuCopyService.ts` | copyScreens() 수정 |

### 8.2 참고 파일

| 파일 | 설명 |
|-----|-----|
| `docs/COMPONENT_LAYOUT_V2_ARCHITECTURE.md` | V2 아키텍처 문서 |
| `frontend/lib/api/screen.ts` | getLayoutV2, saveLayoutV2 |
| `frontend/lib/utils/layoutV2Converter.ts` | V2 변환 유틸리티 |

---

## 9. 체크리스트

### 9.1 개발 전

- [ ] V2 아키텍처 문서 숙지
- [ ] 현재 복제 로직 코드 리뷰
- [ ] 테스트 데이터 준비 (V2 레이아웃이 있는 화면)

### 9.2 Phase 1 완료 조건

- [x] copyLayoutV2() 함수 구현 ✅ 2026-01-28
- [x] collectReferencesFromLayoutV2() 함수 구현 ✅ 2026-01-28
- [x] updateReferencesInLayoutV2() 함수 구현 ✅ 2026-01-28
- [x] copyScreen() - Legacy 제거, V2로 교체 ✅ 2026-01-28
- [x] copyScreens() - Legacy 제거, V2로 교체 ✅ 2026-01-28
- [x] hasLayoutChangesV2() 함수 추가 ✅ 2026-01-28
- [x] updateTabScreenReferences() V2 지원 추가 ✅ 2026-01-28
- [x] 단위 테스트 통과 ✅ 2026-01-30
- [x] 통합 테스트 통과 ✅ 2026-01-30
- [x] V2 전용 복제 동작 확인 ✅ 2026-01-30

### 9.3 Phase 2 완료 조건

- [ ] Legacy 관련 헬퍼 함수 정리
- [ ] 불필요한 코드 제거
- [ ] 코드 리뷰 완료
- [ ] 회귀 테스트 통과

---

## 10. 시뮬레이션 검증 결과

### 10.1 검증된 시나리오

| 시나리오 | 결과 | 비고 |
|---------|------|------|
| 같은 회사 내 복제 | ✅ 정상 | componentId만 새로 생성 |
| 회사 간 복제 (flowId 매핑) | ✅ 정상 | flowIdMap 적용됨 |
| 회사 간 복제 (ruleId 매핑) | ✅ 정상 | ruleIdMap 적용됨 |
| 탭 컴포넌트 screenId 매핑 | ✅ 정상 | updateTabScreenReferences V2 지원 추가 |
| V2 레이아웃 없는 화면 | ✅ 정상 | 스킵 처리 |

### 10.2 발견 및 수정된 문제

| 문제 | 해결 |
|-----|------|
| updateTabScreenReferences가 V2 미지원 | V2 처리 로직 추가 완료 |

### 10.3 Zod 활용 가능성

프론트엔드에 이미 훌륭한 Zod 유틸리티 존재:
- `deepMerge()` - 깊은 병합
- `extractCustomConfig()` - 차이값 추출
- `loadComponentV2()` / `saveComponentV2()` - V2 로드/저장

향후 백엔드에도 Zod 추가 시:
- 타입 안전성 향상
- 프론트/백엔드 스키마 공유 가능
- 범용 참조 탐색 로직으로 하드코딩 제거 가능

---

## 11. 변경 이력

| 날짜 | 변경 내용 | 작성자 |
|-----|----------|-------|
| 2026-01-28 | 초안 작성 | Claude |
| 2026-01-28 | V2 완전 전환 전략으로 변경 (병행 운영 → V2 전용) | Claude |
| 2026-01-28 | Phase 1 구현 완료 - V2 복제 함수들 구현 및 Legacy 교체 | Claude |
| 2026-01-28 | 시뮬레이션 검증 - updateTabScreenReferences V2 지원 추가 | Claude |
| 2026-01-28 | V2 경로 지원 추가 - action/sections 직접 경로 (componentConfig 없이) | Claude |
| 2026-01-30 | **실제 코드 구현 완료** - copyScreen(), copyScreens() V2 전환 | Claude |
| 2026-01-30 | **Phase 1 테스트 완료** - 단위/통합 테스트 통과 확인 | Claude |