# 컴포넌트 레이아웃 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/v2-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 |
