ERP-node/docs/SCREEN_COPY_V2_MIGRATION_PL...

16 KiB

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

작성일: 2026-01-28

1. 현황 분석

1.1 현재 복제 방식 (Legacy)

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

데이터 구조:

-- 화면당 여러 레코드
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 (차이값만)

데이터 구조:

-- 화면당 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 새로 추가할 함수들

// 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):

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

After (V2):

// 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):

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

After (V2):

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

5.2 copyLayoutV2() 구현 방안

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 완료 조건

  • copyLayoutV2() 함수 구현 2026-01-28
  • collectReferencesFromLayoutV2() 함수 구현 2026-01-28
  • updateReferencesInLayoutV2() 함수 구현 2026-01-28
  • copyScreen() - Legacy 제거, V2로 교체 2026-01-28
  • copyScreens() - Legacy 제거, V2로 교체 2026-01-28
  • hasLayoutChangesV2() 함수 추가 2026-01-28
  • updateTabScreenReferences() V2 지원 추가 2026-01-28
  • 단위 테스트 통과 2026-01-30
  • 통합 테스트 통과 2026-01-30
  • 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