ㅡㄹ ㅣ # 컴포넌트 URL 시스템 구현 완료

## 실행 일시: 2026-01-27

## 1. 목표

- 컴포넌트 코드 수정 시 **모든 회사에 즉시 반영** ✅
- 회사별 고유 설정은 **JSON으로 안전하게 관리** (Zod 검증) ✅
- 기존 화면 **100% 동일하게 렌더링** 보장 ✅

---

## 2. 완료된 작업

### 2.1 DB 테이블 생성
- `screen_layouts_v3` 테이블 생성 완료
- 4,414개 레코드 마이그레이션 완료

### 2.2 파일 생성/수정
| 파일 | 상태 |
|-----|-----|
| `frontend/lib/schemas/componentConfig.ts` | ✅ 신규 생성 |
| `backend-node/src/services/screenManagementService.ts` | ✅ getLayoutV3 추가 |
| `backend-node/src/controllers/screenManagementController.ts` | ✅ getLayoutV3 추가 |
| `backend-node/src/routes/screenManagementRoutes.ts` | ✅ 라우트 추가 |

### 2.3 API 엔드포인트
```
GET /api/screen-management/screens/:screenId/layout-v3
```

---

## 3. 핵심 구조

### 2.1 컴포넌트 코드 (파일 시스템)

```
frontend/lib/registry/components/{component-name}/
├── index.ts              # 렌더링 로직, UI
├── schema.ts             # Zod 스키마 + 기본값
└── types.ts              # 타입 정의
```

### 2.2 DB 구조

```sql
screen_layouts_v3 (
  layout_id SERIAL PRIMARY KEY,
  screen_id INTEGER REFERENCES screen_definitions(screen_id),
  component_id VARCHAR(100) UNIQUE NOT NULL,
  
  -- 컴포넌트 URL (파일 경로)
  component_url VARCHAR(200) NOT NULL,
  -- 예: "@/lib/registry/components/split-panel-layout"
  
  -- 회사별 커스텀 설정 (비즈니스 데이터만)
  custom_config JSONB NOT NULL DEFAULT '{}',
  
  -- 레이아웃 정보
  parent_id VARCHAR(100),
  position_x INTEGER NOT NULL DEFAULT 0,
  position_y INTEGER NOT NULL DEFAULT 0,
  width INTEGER NOT NULL DEFAULT 100,
  height INTEGER NOT NULL DEFAULT 100,
  display_order INTEGER DEFAULT 0,
  
  -- 기타
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
)
```

---

## 3. 대상 컴포넌트 (고수준)

| 컴포넌트 | 개수 | 우선순위 |
|---------|-----|---------|
| split-panel-layout | 129 | 높음 |
| tabs-widget | 74 | 높음 |
| modal-repeater-table | 68 | 높음 |
| category-manager | 69 | 중간 |
| flow-widget | 11 | 중간 |
| table-list | 280 | 높음 |
| table-search-widget | 353 | 높음 |
| conditional-container | 53 | 중간 |
| selected-items-detail-input | 83 | 중간 |

---

## 4. 작업 단계

### Phase 1: 스키마 정의
- [ ] split-panel-layout/schema.ts
- [ ] tabs-widget/schema.ts
- [ ] modal-repeater-table/schema.ts
- [ ] table-list/schema.ts
- [ ] table-search-widget/schema.ts
- [ ] 기타 컴포넌트들

### Phase 2: DB 테이블 생성
- [ ] screen_layouts_v3 테이블 생성
- [ ] 인덱스 생성

### Phase 3: 마이그레이션
- [ ] 기존 데이터에서 component_url 추출
- [ ] 기존 데이터에서 custom_config 분리
- [ ] 검증 (기존 화면과 동일 렌더링)

### Phase 4: 백엔드 수정
- [ ] getLayoutV3 API 추가
- [ ] saveLayoutV3 API 추가

### Phase 5: 프론트엔드 수정
- [ ] 렌더링 로직에 스키마 병합 적용
- [ ] 화면 디자이너 저장 로직 수정

---

## 5. Zod 스키마 설계 원칙

### 5.1 기본값 (코드에서 관리)
```typescript
// 컴포넌트 UI/동작 관련 - 코드 수정 시 전체 반영
const baseDefaults = {
  resizable: true,
  splitRatio: 30,
  syncSelection: true,
};
```

### 5.2 커스텀 설정 (DB에서 관리)
```typescript
// 비즈니스 데이터 - 회사별 개별 관리
const customConfigSchema = z.object({
  leftPanel: z.object({
    title: z.string().optional(),
    tableName: z.string(),
    columns: z.array(z.any()).default([]),
  }).passthrough(),
  rightPanel: z.object({
    title: z.string().optional(),
    tableName: z.string(),
    relation: z.any().optional(),
  }).passthrough(),
}).passthrough();
```

### 5.3 병합 로직
```typescript
function mergeConfig(baseDefaults: any, customConfig: any) {
  // 1. 스키마로 customConfig 파싱 (없는 필드는 기본값)
  const parsed = customConfigSchema.parse(customConfig);
  
  // 2. 기본값과 병합
  return { ...baseDefaults, ...parsed };
}
```

---

## 6. 렌더링 흐름

```
1. DB 조회
   ├─ component_url: "@/lib/registry/components/split-panel-layout"
   └─ custom_config: { leftPanel: { tableName: "sales_order_mng", ... } }

2. 컴포넌트 로드
   └─ ComponentRegistry.get("split-panel-layout")

3. 스키마 로드
   └─ import { schema, baseDefaults } from "./schema"

4. 설정 병합
   └─ baseDefaults + schema.parse(custom_config)

5. 렌더링
   └─ <SplitPanelLayout config={mergedConfig} />
```

---

## 7. 마이그레이션 전략

### 7.1 component_url 추출
```sql
-- properties.componentType → component_url 변환
UPDATE screen_layouts_v3
SET component_url = '@/lib/registry/components/' || (properties->>'componentType')
WHERE properties->>'componentType' IS NOT NULL;
```

### 7.2 custom_config 분리
```javascript
// 기존 componentConfig에서 비즈니스 데이터만 추출
function extractCustomConfig(componentType, componentConfig) {
  const baseKeys = getBaseKeys(componentType);  // 코드 기본값 키들
  const customConfig = {};
  
  for (const key of Object.keys(componentConfig)) {
    if (!baseKeys.includes(key)) {
      customConfig[key] = componentConfig[key];
    }
  }
  
  return customConfig;
}
```

### 7.3 검증
```javascript
// 기존 렌더링과 동일한지 확인
function verify(original, migrated) {
  const originalRender = renderWithConfig(original.componentConfig);
  const migratedRender = renderWithConfig(
    merge(baseDefaults, migrated.custom_config)
  );
  
  return deepEqual(originalRender, migratedRender);
}
```

---

## 8. 체크리스트

- [ ] 컴포넌트 코드 수정 → 전체 회사 즉시 반영 확인
- [ ] 기존 고유 설정 100% 유지 확인
- [ ] 새 필드 추가 시 기본값 자동 적용 확인
- [ ] 기존 화면 렌더링 동일성 확인
- [ ] 화면 디자이너 저장/로드 정상 동작 확인
