# 컴포넌트 레이아웃 V2 아키텍처 > 최종 업데이트: 2026-01-27 ## 1. 개요 ### 1.1 목표 - **핵심 목표**: 컴포넌트 코드 수정 시 모든 화면에 자동 반영 - **문제 해결**: 기존 JSON "박제" 방식으로 인한 코드 수정 미반영 문제 - **방식**: 1 레코드 방식 (화면당 1개 레코드, JSON에 모든 컴포넌트 포함) ### 1.2 핵심 원칙 ``` 저장: component_url + overrides (차이값만) 로드: 코드 기본값 + overrides 병합 (Zod) ``` **이전 방식 (문제점)**: ```json // 전체 설정 박제 → 코드 수정해도 반영 안 됨 { "componentType": "table-list", "componentConfig": { "columns": [...], "pagination": true, "pageSize": 20, // ... 수백 줄의 설정 } } ``` **V2 방식 (해결)**: ```json // url로 코드 참조 + 차이값만 저장 { "url": "@/lib/registry/components/table-list", "overrides": { "tableName": "user_info", "columns": ["id", "name"] } } ``` --- ## 2. 데이터베이스 구조 ### 2.1 테이블 정의 ```sql 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, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(screen_id, company_code) ); -- 인덱스 CREATE INDEX idx_v2_screen_id ON screen_layouts_v2(screen_id); CREATE INDEX idx_v2_company_code ON screen_layouts_v2(company_code); CREATE INDEX idx_v2_screen_company ON screen_layouts_v2(screen_id, company_code); ``` ### 2.2 layout_data 구조 ```json { "version": "2.0", "components": [ { "id": "comp_xxx", "url": "@/lib/registry/components/table-list", "position": { "x": 0, "y": 0 }, "size": { "width": 100, "height": 50 }, "displayOrder": 0, "overrides": { "tableName": "user_info", "columns": ["id", "name", "email"] } }, { "id": "comp_yyy", "url": "@/lib/registry/components/button-primary", "position": { "x": 0, "y": 60 }, "size": { "width": 20, "height": 5 }, "displayOrder": 1, "overrides": { "label": "저장", "variant": "default" } } ], "updatedAt": "2026-01-27T12:00:00Z" } ``` ### 2.3 필드 설명 | 필드 | 타입 | 설명 | |-----|-----|-----| | `id` | string | 컴포넌트 고유 ID | | `url` | string | 컴포넌트 코드 경로 (필수) | | `position` | object | 캔버스 내 위치 {x, y} | | `size` | object | 크기 {width, height} | | `displayOrder` | number | 렌더링 순서 | | `overrides` | object | 기본값과 다른 설정만 (차이값) | --- ## 3. API 정의 ### 3.1 레이아웃 조회 ``` GET /api/screen-management/screens/:screenId/layout-v2 ``` **응답**: ```json { "success": true, "data": { "version": "2.0", "components": [...] } } ``` **로직**: 1. 회사별 레이아웃 먼저 조회 2. 없으면 공통(*) 레이아웃 조회 3. 없으면 null 반환 ### 3.2 레이아웃 저장 ``` POST /api/screen-management/screens/:screenId/layout-v2 ``` **요청**: ```json { "components": [ { "id": "comp_xxx", "url": "@/lib/registry/components/table-list", "position": { "x": 0, "y": 0 }, "size": { "width": 100, "height": 50 }, "overrides": { ... } } ] } ``` **로직**: 1. 권한 확인 2. 버전 정보 추가 3. UPSERT (있으면 업데이트, 없으면 삽입) --- ## 4. 컴포넌트 URL 규칙 ### 4.1 URL 형식 ``` @/lib/registry/components/{component-name} ``` ### 4.2 현재 등록된 컴포넌트 | URL | 설명 | |-----|-----| | `@/lib/registry/components/table-list` | 테이블 리스트 | | `@/lib/registry/components/button-primary` | 기본 버튼 | | `@/lib/registry/components/text-input` | 텍스트 입력 | | `@/lib/registry/components/select-basic` | 기본 셀렉트 | | `@/lib/registry/components/date-input` | 날짜 입력 | | `@/lib/registry/components/split-panel-layout` | 분할 패널 | | `@/lib/registry/components/tabs-widget` | 탭 위젯 | | `@/lib/registry/components/card-display` | 카드 디스플레이 | | `@/lib/registry/components/flow-widget` | 플로우 위젯 | | `@/lib/registry/components/category-management` | 카테고리 관리 | | `@/lib/registry/components/pivot-table` | 피벗 테이블 | | `@/lib/registry/components/unified-grid` | 통합 그리드 | --- ## 5. Zod 스키마 관리 ### 5.1 목적 - 런타임 타입 검증 - 기본값 자동 적용 - overrides 유효성 검사 ### 5.2 구조 ```typescript // frontend/lib/schemas/componentConfig.ts import { z } from "zod"; // 공통 스키마 export const baseComponentSchema = z.object({ id: z.string(), url: z.string(), position: z.object({ x: z.number().default(0), y: z.number().default(0), }), size: z.object({ width: z.number().default(100), height: z.number().default(100), }), displayOrder: z.number().default(0), overrides: z.record(z.any()).default({}), }); // 컴포넌트별 overrides 스키마 export const tableListOverridesSchema = z.object({ tableName: z.string().optional(), columns: z.array(z.string()).optional(), pagination: z.boolean().default(true), pageSize: z.number().default(20), }); export const buttonOverridesSchema = z.object({ label: z.string().default("버튼"), variant: z.enum(["default", "destructive", "outline", "ghost"]).default("default"), icon: z.string().optional(), }); ``` ### 5.3 사용 방법 ```typescript // 로드 시: 코드 기본값 + overrides 병합 function loadComponent(component: any) { const schema = getSchemaByUrl(component.url); const defaults = schema.parse({}); const merged = deepMerge(defaults, component.overrides); return merged; } // 저장 시: 기본값과 다른 부분만 추출 function saveComponent(component: any, config: any) { const schema = getSchemaByUrl(component.url); const defaults = schema.parse({}); const overrides = extractDiff(defaults, config); return { ...component, overrides }; } ``` --- ## 6. 마이그레이션 현황 ### 6.1 완료된 작업 | 작업 | 상태 | 날짜 | |-----|-----|-----| | screen_layouts_v2 테이블 생성 | ✅ 완료 | 2026-01-27 | | 기존 데이터 마이그레이션 | ✅ 완료 | 2026-01-27 | | 백엔드 API 추가 (getLayoutV2, saveLayoutV2) | ✅ 완료 | 2026-01-27 | | 프론트엔드 API 클라이언트 추가 | ✅ 완료 | 2026-01-27 | | Zod 스키마 V2 확장 | ✅ 완료 | 2026-01-27 | | V2 변환 유틸리티 (layoutV2Converter.ts) | ✅ 완료 | 2026-01-27 | | ScreenDesigner V2 API 연동 | ✅ 완료 | 2026-01-27 | ### 6.2 마이그레이션 통계 ``` 마이그레이션 대상 화면: 1,347개 성공: 1,347개 (100%) 실패: 0개 컴포넌트 많은 화면 TOP 5: - screen 74: 25개 컴포넌트 - screen 1204: 18개 컴포넌트 - screen 1242: 18개 컴포넌트 - screen 119: 18개 컴포넌트 - screen 1255: 18개 컴포넌트 ``` --- ## 7. 남은 작업 ### 7.1 필수 작업 | 작업 | 우선순위 | 예상 공수 | 상태 | |-----|---------|---------|------| | 프론트엔드 디자이너 V2 API 연동 | 높음 | 3일 | ✅ 완료 | | Zod 스키마 컴포넌트별 정의 | 높음 | 2일 | ✅ 완료 | | V2 변환 유틸리티 | 높음 | 1일 | ✅ 완료 | | 테스트 및 검증 | 중간 | 2일 | 🔄 진행 필요 | ### 7.2 선택 작업 | 작업 | 우선순위 | 예상 공수 | |-----|---------|---------| | 기존 API (layout, layout-v1) 제거 | 낮음 | 1일 | | 기존 테이블 (screen_layouts, screen_layouts_v1) 정리 | 낮음 | 1일 | | 마이그레이션 검증 도구 | 낮음 | 1일 | | 컴포넌트별 기본값 레지스트리 확장 | 낮음 | 2일 | --- ## 8. 개발 가이드 ### 8.1 새 컴포넌트 추가 시 1. **컴포넌트 코드 생성** ``` frontend/lib/registry/components/{component-name}/ ├── index.ts ├── {ComponentName}Renderer.tsx └── types.ts ``` 2. **Zod 스키마 정의** ```typescript // frontend/lib/schemas/components/{component-name}.ts export const {componentName}OverridesSchema = z.object({ // 컴포넌트 고유 설정 }); ``` 3. **레지스트리 등록** ```typescript // frontend/lib/registry/components/index.ts export { default as {ComponentName} } from "./{component-name}"; ``` ### 8.2 화면 저장 시 ```typescript // 디자이너에서 저장 시 async function handleSave() { const layoutData = { components: components.map(comp => ({ id: comp.id, url: comp.url, position: comp.position, size: comp.size, displayOrder: comp.displayOrder, overrides: extractOverrides(comp.url, comp.config) // 차이값만 추출 })) }; await screenApi.saveLayoutV2(screenId, layoutData); } ``` ### 8.3 화면 로드 시 ```typescript // 화면 렌더러에서 로드 시 async function loadScreen(screenId: number) { const layoutData = await screenApi.getLayoutV2(screenId); const components = layoutData.components.map(comp => { const defaults = getDefaultsByUrl(comp.url); // Zod 기본값 const mergedConfig = deepMerge(defaults, comp.overrides); return { ...comp, config: mergedConfig }; }); return components; } ``` --- ## 9. 비교: 기존 vs V2 | 항목 | 기존 (다중 레코드) | V2 (1 레코드) | |-----|------------------|--------------| | 레코드 수 | 화면당 N개 (컴포넌트 수) | 화면당 1개 | | 저장 방식 | 전체 설정 박제 | url + overrides | | 코드 수정 반영 | ❌ 안 됨 | ✅ 자동 반영 | | 중복 데이터 | 있음 (DB 컬럼 + JSON) | 없음 | | 공사량 | - | 테이블 변경 필요 | --- ## 10. 관련 파일 ### 10.1 백엔드 - `backend-node/src/services/screenManagementService.ts` - getLayoutV2, saveLayoutV2 - `backend-node/src/controllers/screenManagementController.ts` - API 엔드포인트 - `backend-node/src/routes/screenManagementRoutes.ts` - 라우트 정의 ### 10.2 프론트엔드 - `frontend/lib/api/screen.ts` - getLayoutV2, saveLayoutV2 클라이언트 - `frontend/lib/schemas/componentConfig.ts` - Zod 스키마 및 V2 유틸리티 - `frontend/lib/utils/layoutV2Converter.ts` - V2 ↔ Legacy 변환 유틸리티 - `frontend/components/screen/ScreenDesigner.tsx` - V2 API 연동 (USE_V2_API 플래그) - `frontend/lib/registry/components/` - 컴포넌트 레지스트리 ### 10.3 데이터베이스 - `screen_layouts_v2` - V2 레이아웃 테이블 --- ## 11. FAQ ### Q1: 기존 화면은 어떻게 되나요? 기존 화면은 마이그레이션되어 `screen_layouts_v2`에 저장됩니다. 디자이너가 V2 API를 사용하도록 수정되면 자동으로 새 구조를 사용합니다. ### Q2: 컴포넌트 코드를 수정하면 정말 전체 반영되나요? 네. `overrides`에는 차이값만 저장되고, 로드 시 코드의 기본값과 병합됩니다. 기본값을 수정하면 모든 화면에 반영됩니다. ### Q3: 회사별 설정은 어떻게 관리하나요? `company_code` 컬럼으로 회사별 레이아웃을 분리합니다. 회사별 레이아웃이 없으면 공통(*) 레이아웃을 사용합니다. ### Q4: 기존 테이블(screen_layouts)은 언제 삭제하나요? V2가 안정화되고 모든 기능이 정상 동작하는지 확인된 후에 삭제합니다. 최소 1개월 이상 병행 운영 권장. --- ## 12. 변경 이력 | 날짜 | 변경 내용 | 작성자 | |-----|----------|-------| | 2026-01-27 | 초안 작성, 테이블 생성, 마이그레이션, API 추가 | Claude | | 2026-01-27 | Zod 스키마 V2 확장, 변환 유틸리티, ScreenDesigner 연동 | Claude |