673 lines
19 KiB
Markdown
673 lines
19 KiB
Markdown
|
|
# 컴포넌트 시스템 마이그레이션 계획서
|
||
|
|
|
||
|
|
## 1. 개요
|
||
|
|
|
||
|
|
### 1.1 목적
|
||
|
|
- 현재 JSON 기반 컴포넌트 관리 시스템을 URL 참조 + Zod 스키마 기반으로 전환
|
||
|
|
- 컴포넌트 코드 수정 시 모든 회사에 즉시 반영되는 구조로 개선
|
||
|
|
- JSON 구조 표준화 및 런타임 검증 체계 구축
|
||
|
|
|
||
|
|
### 1.2 핵심 원칙
|
||
|
|
1. **화면 동일성 유지**: 마이그레이션 전후 렌더링 결과가 100% 동일해야 함
|
||
|
|
2. **안전한 테스트**: 기존 테이블 수정 없이 새 테이블에서 테스트
|
||
|
|
3. **롤백 가능**: 문제 발생 시 즉시 원복 가능한 구조
|
||
|
|
|
||
|
|
### 1.3 현재 상태 (DB 분석 결과)
|
||
|
|
|
||
|
|
| 항목 | 수치 |
|
||
|
|
|-----|-----|
|
||
|
|
| 총 레코드 | 7,170개 |
|
||
|
|
| 화면 수 | 1,363개 |
|
||
|
|
| 회사 수 | 15개 |
|
||
|
|
| 컴포넌트 타입 | 50개 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 테이블 구조
|
||
|
|
|
||
|
|
### 2.1 기존 테이블: `screen_layouts`
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE screen_layouts (
|
||
|
|
layout_id SERIAL PRIMARY KEY,
|
||
|
|
screen_id INTEGER REFERENCES screen_definitions(screen_id),
|
||
|
|
component_type VARCHAR(50) NOT NULL,
|
||
|
|
component_id VARCHAR(100) UNIQUE NOT NULL,
|
||
|
|
parent_id VARCHAR(100),
|
||
|
|
position_x INTEGER NOT NULL,
|
||
|
|
position_y INTEGER NOT NULL,
|
||
|
|
width INTEGER NOT NULL,
|
||
|
|
height INTEGER NOT NULL,
|
||
|
|
properties JSONB, -- 전체 설정이 포함됨
|
||
|
|
display_order INTEGER DEFAULT 0,
|
||
|
|
layout_type VARCHAR(50),
|
||
|
|
layout_config JSONB,
|
||
|
|
zones_config JSONB,
|
||
|
|
zone_id VARCHAR(100)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 신규 테이블: `screen_layouts_v2` (테스트용)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE screen_layouts_v2 (
|
||
|
|
layout_id SERIAL PRIMARY KEY,
|
||
|
|
screen_id INTEGER REFERENCES screen_definitions(screen_id),
|
||
|
|
component_type VARCHAR(50) NOT NULL,
|
||
|
|
component_id VARCHAR(100) UNIQUE NOT NULL,
|
||
|
|
parent_id VARCHAR(100),
|
||
|
|
position_x INTEGER NOT NULL,
|
||
|
|
position_y INTEGER NOT NULL,
|
||
|
|
width INTEGER NOT NULL,
|
||
|
|
height INTEGER NOT NULL,
|
||
|
|
|
||
|
|
-- 변경된 부분
|
||
|
|
component_ref VARCHAR(100) NOT NULL, -- 컴포넌트 URL 참조 (예: "button-primary")
|
||
|
|
config_overrides JSONB DEFAULT '{}', -- 기본값과 다른 설정만 저장
|
||
|
|
|
||
|
|
-- 기존 필드 유지
|
||
|
|
properties JSONB, -- 기존 호환용 (마이그레이션 완료 후 제거)
|
||
|
|
display_order INTEGER DEFAULT 0,
|
||
|
|
layout_type VARCHAR(50),
|
||
|
|
layout_config JSONB,
|
||
|
|
zones_config JSONB,
|
||
|
|
zone_id VARCHAR(100),
|
||
|
|
|
||
|
|
-- 마이그레이션 추적
|
||
|
|
migrated_at TIMESTAMPTZ,
|
||
|
|
migration_status VARCHAR(20) DEFAULT 'pending' -- pending, success, failed
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 마이그레이션 단계
|
||
|
|
|
||
|
|
### 3.1 Phase 1: 테이블 생성 및 데이터 복사
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Step 1: 새 테이블 생성
|
||
|
|
CREATE TABLE screen_layouts_v2 AS
|
||
|
|
SELECT * FROM screen_layouts;
|
||
|
|
|
||
|
|
-- Step 2: 새 컬럼 추가
|
||
|
|
ALTER TABLE screen_layouts_v2
|
||
|
|
ADD COLUMN component_ref VARCHAR(100),
|
||
|
|
ADD COLUMN config_overrides JSONB DEFAULT '{}',
|
||
|
|
ADD COLUMN migrated_at TIMESTAMPTZ,
|
||
|
|
ADD COLUMN migration_status VARCHAR(20) DEFAULT 'pending';
|
||
|
|
|
||
|
|
-- Step 3: component_ref 초기값 설정
|
||
|
|
UPDATE screen_layouts_v2
|
||
|
|
SET component_ref = properties->>'componentType'
|
||
|
|
WHERE properties->>'componentType' IS NOT NULL;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Phase 2: Zod 스키마 정의
|
||
|
|
|
||
|
|
각 컴포넌트별 스키마 파일 생성:
|
||
|
|
|
||
|
|
```
|
||
|
|
frontend/lib/schemas/components/
|
||
|
|
├── button-primary.schema.ts
|
||
|
|
├── text-input.schema.ts
|
||
|
|
├── table-list.schema.ts
|
||
|
|
├── select-basic.schema.ts
|
||
|
|
├── date-input.schema.ts
|
||
|
|
├── file-upload.schema.ts
|
||
|
|
├── tabs-widget.schema.ts
|
||
|
|
├── split-panel-layout.schema.ts
|
||
|
|
├── flow-widget.schema.ts
|
||
|
|
└── ... (50개)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 Phase 3: 차이값 추출
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 마이그레이션 스크립트 (backend-node)
|
||
|
|
async function extractConfigDiff(layoutId: number) {
|
||
|
|
const layout = await getLayoutById(layoutId);
|
||
|
|
const componentType = layout.properties?.componentType;
|
||
|
|
|
||
|
|
if (!componentType) {
|
||
|
|
return { status: 'skip', reason: 'no componentType' };
|
||
|
|
}
|
||
|
|
|
||
|
|
// 스키마에서 기본값 가져오기
|
||
|
|
const schema = getSchemaByType(componentType);
|
||
|
|
const defaults = schema.parse({});
|
||
|
|
|
||
|
|
// 현재 저장된 설정
|
||
|
|
const currentConfig = layout.properties?.componentConfig || {};
|
||
|
|
|
||
|
|
// 기본값과 다른 것만 추출
|
||
|
|
const overrides = extractDifferences(defaults, currentConfig);
|
||
|
|
|
||
|
|
return {
|
||
|
|
status: 'success',
|
||
|
|
component_ref: componentType,
|
||
|
|
config_overrides: overrides,
|
||
|
|
original_config: currentConfig
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.4 Phase 4: 렌더링 동일성 검증
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 검증 스크립트
|
||
|
|
async function verifyRenderingEquality(layoutId: number) {
|
||
|
|
// 기존 방식으로 로드
|
||
|
|
const originalConfig = await loadOriginalConfig(layoutId);
|
||
|
|
|
||
|
|
// 새 방식으로 로드 (기본값 + overrides 병합)
|
||
|
|
const migratedConfig = await loadMigratedConfig(layoutId);
|
||
|
|
|
||
|
|
// 깊은 비교
|
||
|
|
const isEqual = deepEqual(originalConfig, migratedConfig);
|
||
|
|
|
||
|
|
if (!isEqual) {
|
||
|
|
const diff = getDifferences(originalConfig, migratedConfig);
|
||
|
|
console.error(`Layout ${layoutId} 불일치:`, diff);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 컴포넌트별 분석
|
||
|
|
|
||
|
|
### 4.1 상위 10개 컴포넌트 (우선 처리)
|
||
|
|
|
||
|
|
| 순위 | 컴포넌트 | 개수 | JSON 일관성 | 복잡도 |
|
||
|
|
|-----|---------|-----|------------|-------|
|
||
|
|
| 1 | button-primary | 1,527 | 100% | 낮음 |
|
||
|
|
| 2 | text-input | 700 | 95% | 낮음 |
|
||
|
|
| 3 | table-search-widget | 353 | 100% | 중간 |
|
||
|
|
| 4 | table-list | 280 | 84% | 높음 |
|
||
|
|
| 5 | file-upload | 143 | 100% | 중간 |
|
||
|
|
| 6 | select-basic | 129 | 100% | 낮음 |
|
||
|
|
| 7 | split-panel-layout | 129 | 100% | 높음 |
|
||
|
|
| 8 | date-input | 116 | 100% | 낮음 |
|
||
|
|
| 9 | unified-list | 97 | 100% | 높음 |
|
||
|
|
| 10 | number-input | 87 | 100% | 낮음 |
|
||
|
|
|
||
|
|
### 4.2 발견된 문제점
|
||
|
|
|
||
|
|
#### 문제 1: componentType ≠ componentConfig.type
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 166개 불일치 발견
|
||
|
|
SELECT COUNT(*) FROM screen_layouts
|
||
|
|
WHERE properties->>'componentType' = 'text-input'
|
||
|
|
AND properties->'componentConfig'->>'type' != 'text-input';
|
||
|
|
```
|
||
|
|
|
||
|
|
**해결**: 마이그레이션 시 `componentConfig.type`을 `componentType`으로 통일
|
||
|
|
|
||
|
|
#### 문제 2: 키 누락 (table-list)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 44개 (16%) pagination/checkbox 없음
|
||
|
|
SELECT COUNT(*) FROM screen_layouts
|
||
|
|
WHERE properties->>'componentType' = 'table-list'
|
||
|
|
AND properties->'componentConfig' ? 'pagination' = false;
|
||
|
|
```
|
||
|
|
|
||
|
|
**해결**: 누락된 키는 기본값으로 자동 채움 (Zod 스키마 활용)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Zod 스키마 예시
|
||
|
|
|
||
|
|
### 5.1 button-primary
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/lib/schemas/components/button-primary.schema.ts
|
||
|
|
import { z } from "zod";
|
||
|
|
|
||
|
|
export const buttonActionSchema = z.object({
|
||
|
|
type: z.enum([
|
||
|
|
"save", "modal", "openModalWithData", "edit", "delete",
|
||
|
|
"control", "excel_upload", "excel_download", "transferData",
|
||
|
|
"copy", "code_merge", "view_table_history", "quickInsert",
|
||
|
|
"openRelatedModal", "operation_control", "geolocation",
|
||
|
|
"update_field", "search", "submit", "cancel", "add",
|
||
|
|
"navigate", "empty_vehicle", "reset", "close"
|
||
|
|
]).default("save"),
|
||
|
|
targetScreenId: z.number().optional(),
|
||
|
|
successMessage: z.string().optional(),
|
||
|
|
errorMessage: z.string().optional(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const buttonPrimarySchema = z.object({
|
||
|
|
text: z.string().default("저장"),
|
||
|
|
type: z.literal("button-primary").default("button-primary"),
|
||
|
|
actionType: z.enum(["button", "submit", "reset"]).default("button"),
|
||
|
|
variant: z.enum(["primary", "secondary", "danger"]).default("primary"),
|
||
|
|
webType: z.literal("button").default("button"),
|
||
|
|
action: buttonActionSchema.optional(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export type ButtonPrimaryConfig = z.infer<typeof buttonPrimarySchema>;
|
||
|
|
export const buttonPrimaryDefaults = buttonPrimarySchema.parse({});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 table-list
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// frontend/lib/schemas/components/table-list.schema.ts
|
||
|
|
import { z } from "zod";
|
||
|
|
|
||
|
|
export const paginationSchema = z.object({
|
||
|
|
enabled: z.boolean().default(true),
|
||
|
|
pageSize: z.number().default(20),
|
||
|
|
showSizeSelector: z.boolean().default(true),
|
||
|
|
showPageInfo: z.boolean().default(true),
|
||
|
|
pageSizeOptions: z.array(z.number()).default([10, 20, 50, 100]),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const checkboxSchema = z.object({
|
||
|
|
enabled: z.boolean().default(true),
|
||
|
|
multiple: z.boolean().default(true),
|
||
|
|
position: z.enum(["left", "right"]).default("left"),
|
||
|
|
selectAll: z.boolean().default(true),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const tableListSchema = z.object({
|
||
|
|
type: z.literal("table-list").default("table-list"),
|
||
|
|
webType: z.literal("table").default("table"),
|
||
|
|
displayMode: z.enum(["table", "card"]).default("table"),
|
||
|
|
showHeader: z.boolean().default(true),
|
||
|
|
showFooter: z.boolean().default(true),
|
||
|
|
autoLoad: z.boolean().default(true),
|
||
|
|
autoWidth: z.boolean().default(true),
|
||
|
|
stickyHeader: z.boolean().default(false),
|
||
|
|
height: z.enum(["auto", "fixed", "viewport"]).default("auto"),
|
||
|
|
columns: z.array(z.any()).default([]),
|
||
|
|
pagination: paginationSchema.default({}),
|
||
|
|
checkbox: checkboxSchema.default({}),
|
||
|
|
horizontalScroll: z.object({
|
||
|
|
enabled: z.boolean().default(false),
|
||
|
|
}).default({}),
|
||
|
|
filter: z.object({
|
||
|
|
enabled: z.boolean().default(false),
|
||
|
|
filters: z.array(z.any()).default([]),
|
||
|
|
}).default({}),
|
||
|
|
actions: z.object({
|
||
|
|
showActions: z.boolean().default(false),
|
||
|
|
actions: z.array(z.any()).default([]),
|
||
|
|
bulkActions: z.boolean().default(false),
|
||
|
|
bulkActionList: z.array(z.string()).default([]),
|
||
|
|
}).default({}),
|
||
|
|
tableStyle: z.object({
|
||
|
|
theme: z.enum(["default", "striped", "bordered", "minimal"]).default("default"),
|
||
|
|
headerStyle: z.enum(["default", "dark", "light"]).default("default"),
|
||
|
|
rowHeight: z.enum(["compact", "normal", "comfortable"]).default("normal"),
|
||
|
|
alternateRows: z.boolean().default(false),
|
||
|
|
hoverEffect: z.boolean().default(true),
|
||
|
|
borderStyle: z.enum(["none", "light", "heavy"]).default("light"),
|
||
|
|
}).default({}),
|
||
|
|
});
|
||
|
|
|
||
|
|
export type TableListConfig = z.infer<typeof tableListSchema>;
|
||
|
|
export const tableListDefaults = tableListSchema.parse({});
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 렌더링 로직 변경
|
||
|
|
|
||
|
|
### 6.1 현재 방식
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// DynamicComponentRenderer.tsx (현재)
|
||
|
|
function renderComponent(layout: ScreenLayout) {
|
||
|
|
const config = layout.properties?.componentConfig || {};
|
||
|
|
return <Component config={config} />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 변경 후 방식
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// DynamicComponentRenderer.tsx (변경 후)
|
||
|
|
function renderComponent(layout: ScreenLayoutV2) {
|
||
|
|
const componentRef = layout.component_ref;
|
||
|
|
const overrides = layout.config_overrides || {};
|
||
|
|
|
||
|
|
// 스키마에서 기본값 가져오기
|
||
|
|
const schema = getSchemaByType(componentRef);
|
||
|
|
const defaults = schema.parse({});
|
||
|
|
|
||
|
|
// 기본값 + overrides 병합
|
||
|
|
const config = deepMerge(defaults, overrides);
|
||
|
|
|
||
|
|
return <Component config={config} />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 테스트 계획
|
||
|
|
|
||
|
|
### 7.1 단위 테스트
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
describe("ComponentMigration", () => {
|
||
|
|
test("button-primary 기본값 병합", () => {
|
||
|
|
const overrides = { text: "등록" };
|
||
|
|
const result = mergeWithDefaults("button-primary", overrides);
|
||
|
|
|
||
|
|
expect(result.text).toBe("등록"); // override 값
|
||
|
|
expect(result.variant).toBe("primary"); // 기본값
|
||
|
|
expect(result.actionType).toBe("button"); // 기본값
|
||
|
|
});
|
||
|
|
|
||
|
|
test("table-list 누락된 키 복구", () => {
|
||
|
|
const overrides = { columns: [...] }; // pagination 없음
|
||
|
|
const result = mergeWithDefaults("table-list", overrides);
|
||
|
|
|
||
|
|
expect(result.pagination.enabled).toBe(true);
|
||
|
|
expect(result.pagination.pageSize).toBe(20);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7.2 통합 테스트
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
describe("RenderingEquality", () => {
|
||
|
|
test("모든 레이아웃 렌더링 동일성 검증", async () => {
|
||
|
|
const layouts = await getAllLayouts();
|
||
|
|
|
||
|
|
for (const layout of layouts) {
|
||
|
|
const original = await renderOriginal(layout);
|
||
|
|
const migrated = await renderMigrated(layout);
|
||
|
|
|
||
|
|
expect(migrated).toEqual(original);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 롤백 계획
|
||
|
|
|
||
|
|
### 8.1 즉시 롤백
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 마이그레이션 실패 시 원래 properties 사용
|
||
|
|
UPDATE screen_layouts_v2
|
||
|
|
SET migration_status = 'rollback'
|
||
|
|
WHERE layout_id = ?;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.2 전체 롤백
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 기존 테이블로 복귀
|
||
|
|
DROP TABLE screen_layouts_v2;
|
||
|
|
-- 기존 screen_layouts 계속 사용
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 작업 순서
|
||
|
|
|
||
|
|
### Step 1: 테이블 생성 및 데이터 복사
|
||
|
|
- [ ] `screen_layouts_v2` 테이블 생성
|
||
|
|
- [ ] 기존 데이터 복사
|
||
|
|
- [ ] 새 컬럼 추가
|
||
|
|
|
||
|
|
### Step 2: Zod 스키마 정의 (상위 10개)
|
||
|
|
- [ ] button-primary
|
||
|
|
- [ ] text-input
|
||
|
|
- [ ] table-search-widget
|
||
|
|
- [ ] table-list
|
||
|
|
- [ ] file-upload
|
||
|
|
- [ ] select-basic
|
||
|
|
- [ ] split-panel-layout
|
||
|
|
- [ ] date-input
|
||
|
|
- [ ] unified-list
|
||
|
|
- [ ] number-input
|
||
|
|
|
||
|
|
### Step 3: 마이그레이션 스크립트
|
||
|
|
- [ ] 차이값 추출 함수
|
||
|
|
- [ ] 렌더링 동일성 검증 함수
|
||
|
|
- [ ] 배치 마이그레이션 스크립트
|
||
|
|
|
||
|
|
### Step 4: 테스트
|
||
|
|
- [ ] 단위 테스트
|
||
|
|
- [ ] 통합 테스트
|
||
|
|
- [ ] 화면 렌더링 비교
|
||
|
|
|
||
|
|
### Step 5: 적용
|
||
|
|
- [ ] 프론트엔드 렌더링 로직 수정
|
||
|
|
- [ ] 백엔드 저장 로직 수정
|
||
|
|
- [ ] 기존 테이블 교체
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. 예상 일정
|
||
|
|
|
||
|
|
| 단계 | 작업 | 예상 기간 |
|
||
|
|
|-----|-----|---------|
|
||
|
|
| 1 | 테이블 생성 및 복사 | 1일 |
|
||
|
|
| 2 | 상위 10개 스키마 정의 | 3일 |
|
||
|
|
| 3 | 마이그레이션 스크립트 | 3일 |
|
||
|
|
| 4 | 테스트 및 검증 | 3일 |
|
||
|
|
| 5 | 나머지 40개 스키마 | 5일 |
|
||
|
|
| 6 | 전체 마이그레이션 | 2일 |
|
||
|
|
| 7 | 프론트엔드 적용 | 2일 |
|
||
|
|
| **총계** | | **약 19일 (4주)** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 11. 주의사항
|
||
|
|
|
||
|
|
1. **기존 DB 수정 금지**: 모든 테스트는 `screen_layouts_v2`에서만 진행
|
||
|
|
2. **화면 동일성 우선**: 렌더링 결과가 다르면 마이그레이션 중단
|
||
|
|
3. **단계별 검증**: 각 단계 완료 후 검증 통과해야 다음 단계 진행
|
||
|
|
4. **롤백 대비**: 언제든 기존 시스템으로 복귀 가능해야 함
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 12. 마이그레이션 실행 결과 (2026-01-27)
|
||
|
|
|
||
|
|
### 12.1 실행 환경
|
||
|
|
|
||
|
|
```
|
||
|
|
테이블: screen_layouts_v2 (테스트용)
|
||
|
|
백업: screen_layouts_backup_20260127
|
||
|
|
원본: screen_layouts (변경 없음)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 12.2 마이그레이션 결과
|
||
|
|
|
||
|
|
| 상태 | 개수 | 비율 |
|
||
|
|
|-----|-----|-----|
|
||
|
|
| **success** | 5,805 | 81.0% |
|
||
|
|
| **skip** | 1,365 | 19.0% (metadata) |
|
||
|
|
| **pending** | 0 | 0% |
|
||
|
|
| **fail** | 0 | 0% |
|
||
|
|
|
||
|
|
### 12.3 데이터 절약량
|
||
|
|
|
||
|
|
| 항목 | 수치 |
|
||
|
|
|-----|-----|
|
||
|
|
| 원본 총 크기 | **5.81 MB** |
|
||
|
|
| config_overrides 총 크기 | **2.54 MB** |
|
||
|
|
| **절약량** | **3.27 MB (56.2%)** |
|
||
|
|
|
||
|
|
### 12.4 컴포넌트별 결과
|
||
|
|
|
||
|
|
| 컴포넌트 | 개수 | 원본(bytes) | override(bytes) | 절약률 |
|
||
|
|
|---------|-----|------------|-----------------|-------|
|
||
|
|
| text-input | 1,797 | 701 | 143 | **79.6%** |
|
||
|
|
| button-primary | 1,527 | 939 | 218 | **76.8%** |
|
||
|
|
| table-search-widget | 353 | 635 | 150 | **76.4%** |
|
||
|
|
| select-basic | 287 | 660 | 172 | **73.9%** |
|
||
|
|
| table-list | 280 | 2,690 | 2,020 | 24.9% |
|
||
|
|
| file-upload | 143 | 1,481 | 53 | **96.4%** |
|
||
|
|
| date-input | 137 | 628 | 111 | **82.3%** |
|
||
|
|
| split-panel-layout | 129 | 2,556 | 2,040 | 20.2% |
|
||
|
|
| number-input | 115 | 646 | 121 | **81.2%** |
|
||
|
|
|
||
|
|
### 12.5 config_overrides 구조
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"_originalKeys": ["text", "type", "action", "variant", "webType", "actionType"],
|
||
|
|
"text": "등록",
|
||
|
|
"action": {
|
||
|
|
"type": "modal",
|
||
|
|
"targetScreenId": 26
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- `_originalKeys`: 원본에 있던 키 목록 (복원 시 사용)
|
||
|
|
- 나머지: 기본값과 다른 설정만 저장
|
||
|
|
|
||
|
|
### 12.6 렌더링 복원 로직
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function reconstructConfig(componentRef: string, overrides: any): any {
|
||
|
|
const defaults = getDefaultsByType(componentRef);
|
||
|
|
const originalKeys = overrides._originalKeys || Object.keys(defaults);
|
||
|
|
|
||
|
|
const result = {};
|
||
|
|
for (const key of originalKeys) {
|
||
|
|
if (overrides.hasOwnProperty(key) && key !== '_originalKeys') {
|
||
|
|
result[key] = overrides[key];
|
||
|
|
} else if (defaults.hasOwnProperty(key)) {
|
||
|
|
result[key] = defaults[key];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 12.7 검증 결과
|
||
|
|
|
||
|
|
- **button-primary**: 1,527개 전체 검증 통과 (100%)
|
||
|
|
- **text-input**: 1,797개 전체 검증 통과 (100%)
|
||
|
|
- **table-list**: 280개 전체 검증 통과 (100%)
|
||
|
|
- **기타 모든 컴포넌트**: 전체 검증 통과 (100%)
|
||
|
|
|
||
|
|
### 12.8 다음 단계
|
||
|
|
|
||
|
|
1. [x] ~~Zod 스키마 파일 생성~~ ✅ 완료
|
||
|
|
2. [x] ~~백엔드 API에서 config_overrides 기반 응답 추가~~ ✅ 완료
|
||
|
|
3. [ ] 프론트엔드에서 V2 API 호출 테스트
|
||
|
|
4. [ ] 실제 화면에서 렌더링 테스트
|
||
|
|
5. [ ] screen_layouts 테이블 교체 (운영 적용)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 13. Zod 스키마 파일 생성 완료 (2026-01-27)
|
||
|
|
|
||
|
|
### 13.1 생성된 파일 목록
|
||
|
|
|
||
|
|
```
|
||
|
|
frontend/lib/schemas/components/
|
||
|
|
├── index.ts # 메인 인덱스 + 복원 유틸리티
|
||
|
|
├── button-primary.ts # 버튼 스키마
|
||
|
|
├── text-input.ts # 텍스트 입력 스키마
|
||
|
|
├── table-list.ts # 테이블 리스트 스키마
|
||
|
|
├── select-basic.ts # 셀렉트 스키마
|
||
|
|
├── date-input.ts # 날짜 입력 스키마
|
||
|
|
├── file-upload.ts # 파일 업로드 스키마
|
||
|
|
└── number-input.ts # 숫자 입력 스키마
|
||
|
|
```
|
||
|
|
|
||
|
|
### 13.2 주요 유틸리티 함수
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 컴포넌트 기본값 조회
|
||
|
|
import { getComponentDefaults } from "@/lib/schemas/components";
|
||
|
|
const defaults = getComponentDefaults("button-primary");
|
||
|
|
|
||
|
|
// 설정 복원 (기본값 + overrides 병합)
|
||
|
|
import { reconstructConfig } from "@/lib/schemas/components";
|
||
|
|
const fullConfig = reconstructConfig("button-primary", overrides);
|
||
|
|
|
||
|
|
// 차이값 추출 (저장 시 사용)
|
||
|
|
import { extractConfigDiff } from "@/lib/schemas/components";
|
||
|
|
const diff = extractConfigDiff("button-primary", currentConfig);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 13.3 componentDefaults 레지스트리
|
||
|
|
|
||
|
|
50개 컴포넌트의 기본값이 `componentDefaults` 맵에 등록됨:
|
||
|
|
|
||
|
|
- button-primary, v2-button-primary
|
||
|
|
- text-input, number-input, date-input
|
||
|
|
- select-basic, checkbox-basic, radio-basic
|
||
|
|
- table-list, v2-table-list
|
||
|
|
- tabs-widget, v2-tabs-widget
|
||
|
|
- split-panel-layout, v2-split-panel-layout
|
||
|
|
- flow-widget, category-manager
|
||
|
|
- 기타 40+ 컴포넌트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 14. 백엔드 API 추가 완료 (2026-01-27)
|
||
|
|
|
||
|
|
### 14.1 수정된 파일
|
||
|
|
|
||
|
|
| 파일 | 변경 내용 |
|
||
|
|
|-----|----------|
|
||
|
|
| `backend-node/src/utils/componentDefaults.ts` | 컴포넌트 기본값 + 복원 유틸리티 신규 생성 |
|
||
|
|
| `backend-node/src/services/screenManagementService.ts` | `getLayoutV2()` 함수 추가 |
|
||
|
|
| `backend-node/src/controllers/screenManagementController.ts` | `getLayoutV2` 컨트롤러 추가 |
|
||
|
|
| `backend-node/src/routes/screenManagementRoutes.ts` | `/screens/:screenId/layout-v2` 라우트 추가 |
|
||
|
|
|
||
|
|
### 14.2 새로운 API 엔드포인트
|
||
|
|
|
||
|
|
```
|
||
|
|
GET /api/screen-management/screens/:screenId/layout-v2
|
||
|
|
```
|
||
|
|
|
||
|
|
**응답 구조**: 기존 `getLayout`과 동일
|
||
|
|
|
||
|
|
**차이점**:
|
||
|
|
- `screen_layouts_v2` 테이블에서 조회
|
||
|
|
- `migration_status = 'success'`인 레코드는 `config_overrides` + 기본값 병합
|
||
|
|
- 마이그레이션 안 된 레코드는 기존 `properties.componentConfig` 사용
|
||
|
|
|
||
|
|
### 14.3 복원 로직 흐름
|
||
|
|
|
||
|
|
```
|
||
|
|
1. screen_layouts_v2에서 조회
|
||
|
|
2. migration_status 확인
|
||
|
|
├─ 'success': reconstructConfig(componentRef, configOverrides)
|
||
|
|
└─ 기타: 기존 properties.componentConfig 사용
|
||
|
|
3. 최신 inputType 정보 병합 (table_type_columns)
|
||
|
|
4. 전체 componentConfig 반환
|
||
|
|
```
|
||
|
|
|
||
|
|
### 14.4 테스트 방법
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 기존 API
|
||
|
|
curl "http://localhost:8080/api/screen-management/screens/1/layout" -H "Authorization: Bearer ..."
|
||
|
|
|
||
|
|
# V2 API
|
||
|
|
curl "http://localhost:8080/api/screen-management/screens/1/layout-v2" -H "Authorization: Bearer ..."
|
||
|
|
```
|
||
|
|
|
||
|
|
두 응답의 `components[].componentConfig`가 동일해야 함
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*작성일: 2026-01-27*
|
||
|
|
*작성자: AI Assistant*
|
||
|
|
*버전: 1.1 (마이그레이션 실행 결과 추가)*
|