- Deleted the following files as they are no longer relevant to the current project structure: - 결재 시스템 구현 현황 - 결재 시스템 v2 사용 가이드 - WACE 시스템 문제점 분석 및 개선 계획 - Agent Pipeline 한계점 분석 - AI 기반 화면 자동 생성 시스템 설계서 - WACE ERP Backend - 분석 문서 인덱스 These deletions help streamline the documentation and remove obsolete information, ensuring that only current and relevant resources are maintained.
16 KiB
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 |