Files
vexplor/docs/COMPONENT_LAYOUT_V2_ARCHITECTURE.md
DDD1542 192b678bce fix: 화면 복제 기능 개선 및 관련 버그 수정
- 화면 복제 기능을 개선하여 DB 구조 개편 후의 효율적인 화면 관리를 지원합니다.
- 그룹 복제 시 버튼의 `targetScreenId`가 새 화면으로 매핑되지 않는 버그를 수정하였습니다.
- 관련된 서비스 및 쿼리에서 `table_type_columns`를 사용하여 라벨 정보를 조회하도록 변경하였습니다.
- 여러 컨트롤러 및 서비스에서 `column_labels` 대신 `table_type_columns`를 참조하도록 업데이트하였습니다.
2026-01-28 11:24:25 +09:00

12 KiB

컴포넌트 레이아웃 V2 아키텍처

최종 업데이트: 2026-01-27

1. 개요

1.1 목표

  • 핵심 목표: 컴포넌트 코드 수정 시 모든 화면에 자동 반영
  • 문제 해결: 기존 JSON "박제" 방식으로 인한 코드 수정 미반영 문제
  • 방식: 1 레코드 방식 (화면당 1개 레코드, JSON에 모든 컴포넌트 포함)

1.2 핵심 원칙

저장: component_url + overrides (차이값만)
로드: 코드 기본값 + overrides 병합 (Zod)

이전 방식 (문제점):

// 전체 설정 박제 → 코드 수정해도 반영 안 됨
{
  "componentType": "table-list",
  "componentConfig": {
    "columns": [...],
    "pagination": true,
    "pageSize": 20,
    // ... 수백 줄의 설정
  }
}

V2 방식 (해결):

// url로 코드 참조 + 차이값만 저장
{
  "url": "@/lib/registry/components/table-list",
  "overrides": {
    "tableName": "user_info",
    "columns": ["id", "name"]
  }
}

2. 데이터베이스 구조

2.1 테이블 정의

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 구조

{
  "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

응답:

{
  "success": true,
  "data": {
    "version": "2.0",
    "components": [...]
  }
}

로직:

  1. 회사별 레이아웃 먼저 조회
  2. 없으면 공통(*) 레이아웃 조회
  3. 없으면 null 반환

3.2 레이아웃 저장

POST /api/screen-management/screens/:screenId/layout-v2

요청:

{
  "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 구조

// 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 사용 방법

// 로드 시: 코드 기본값 + 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 스키마 정의

    // frontend/lib/schemas/components/{component-name}.ts
    export const {componentName}OverridesSchema = z.object({
      // 컴포넌트 고유 설정
    });
    
  3. 레지스트리 등록

    // frontend/lib/registry/components/index.ts
    export { default as {ComponentName} } from "./{component-name}";
    

8.2 화면 저장 시

// 디자이너에서 저장 시
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 화면 로드 시

// 화면 렌더러에서 로드 시
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