434 lines
12 KiB
Markdown
434 lines
12 KiB
Markdown
|
|
# 컴포넌트 레이아웃 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 |
|