# INVYONE 컴포넌트 통합 플랜 — v2-* + legacy 86개 → 9종

> **작성일**: 2026-04-11
> **상태**: DRAFT — 사용자 확정 대기
> **작성**: gbpark + Claude
> **관련 문서**:
> - `2026-04-09-invyone-architecture.md` (ComponentType 8종 원안)
> - `2026-04-08-invyone-component-spec.md` (FieldConfig, DataPort, Config 규격)
> - `2026-04-10-card-engine-final-spec.md` (카드 엔진 진실의 원천)

---

## 0. 한 줄 요약

**`frontend/lib/registry/components/` 의 86개 폴더(v2-* 33 + legacy 53)를
9종의 통합 컴포넌트로 물리적으로 합치고 나머지는 삭제한다. 각 그룹은 하나의
Component 파일 + 하나의 ConfigPanel 을 가지며, 변형은 config 필드로만 표현.**

---

## 1. 배경 / 왜 통합이 필요한가

### 1.1 현재 문제
- `v2-input`, `v2-select`, `v2-date`, `v2-category-manager` 등 **실질적으로 "필드 입력"** 역할을 하는 컴포넌트가 10개 이상 분산
- `v2-table-list`, `v2-table-grouped`, `v2-pivot-grid`, `v2-split-panel-layout`, `table-list`(legacy) 등 **"테이블"** 역할이 9개로 분산
- 각자 별도 ConfigPanel / 별도 규격 / 별도 props → **FieldConfig 공유 불가**
- 개발자가 화면 만들 때 "텍스트 입력 → `v2-input`? `text-input`? `v2-text-display`?" 같은 **이름 기반 선택 고문**

### 1.2 통합 후 기대
- 개발자: 팔레트에 "입력" 하나만 보임 → 드롭 후 `FieldConfig.type` 으로 모드 전환 (text / select / date / entity ...)
- 각 컴포넌트 규격이 통일되어 화면 수준 `fields: FieldConfig[]` 가 자동으로 흘러감
- V2PropertiesPanel 도 9종 ConfigPanel 만 다루면 됨 → 80개 분기 사라짐

### 1.3 FieldConfig 가 규격의 축
- 모든 "입력/표시" 계열 컴포넌트는 `FieldConfig` 를 받아서 type 에 따라 자동 렌더
- 이 전제가 통합의 핵심

---

## 2. 통합 방식 — A (물리적 통합) 확정

| 선택 | 설명 |
|---|---|
| **A. 물리적 통합 ✅** | 여러 v2-* / legacy 폴더를 하나의 새 폴더로 합침. 기존 폴더는 완전 삭제. 팔레트에서 영구 제거 |
| ~~B. 논리적 통합~~ | 파일은 유지하되 팔레트에서만 숨김. 버림 |

### 2.1 물리적 통합 원칙
1. **새 통합 폴더 생성** — 예: `components/table/`, `components/input/`, `components/button/`
2. **기존 폴더 내용을 새 폴더로 이관** — 가장 많이 쓰이는 것(예: v2-table-list)을 base 로
3. **기존 폴더 삭제** — (우선 `_obsolete/` 로 이동 → 확인 후 삭제)
4. **id 재지정** — `v2-table-list` 등 접두사 없이 `table`, `input`, `button` 같은 순수 이름
5. **import 경로 치환** — 사용처에서 새 경로로

### 2.2 호환 계층
- 기존 화면의 컴포넌트 type (`v2-table-list` 등) 이 JSON 에 저장돼 있음
- **load 시점에 alias 매핑**: `v2-table-list` → `table`, `v2-input` → `input`, ...
- `ComponentRegistry` 에 legacy alias 테이블 추가

---

## 3. 그룹핑 결정 (9종 + 도메인 특화)

### 3.1 `table` — 데이터 테이블 (1종)

**목적**: 컬럼 기반 데이터 목록. 변형은 displayMode 로.

**통합 대상** (9개 → 1개):

| 기존 ID | 현재 폴더 | 흡수 후 config |
|---|---|---|
| `v2-table-list` | `v2-table-list/` | `displayMode: 'table'` (기본) |
| `v2-table-grouped` | `v2-table-grouped/` | `displayMode: 'grouped'` |
| `v2-pivot-grid` | `v2-pivot-grid/` | `displayMode: 'pivot'` |
| `v2-split-panel-layout` | `v2-split-panel-layout/` | `displayMode: 'split'` (master-detail) |
| `table-list` (legacy) | `table-list/` | 삭제 (v2 이전 버전) |
| `split-panel-layout` | `split-panel-layout/` | 삭제 |
| `split-panel-layout2` | `split-panel-layout2/` | 삭제 |
| `modal-repeater-table` | `modal-repeater-table/` | 삭제 (특수 케이스, 필요 시 config 흡수) |
| `simple-repeater-table` | `simple-repeater-table/` | 삭제 |
| `tax-invoice-list` | `tax-invoice-list/` | 삭제 (도메인 특화, 템플릿으로 처리) |
| `pivot-grid` (legacy) | `pivot-grid/` | 삭제 |

**새 폴더**: `components/table/`
**새 id**: `table`
**config**: `TableConfig` (invyone-component.ts §4.1)

---

### 3.2 `form` — 입력 폼 (1종)

**통합 대상** (2개 → 1개):

| 기존 ID | 폴더 | 액션 |
|---|---|---|
| `form-layout` (legacy, id=`field-example-1`) | `form-layout/` | base, 새 폴더로 이주 |
| `universal-form-modal` | `universal-form-modal/` | 흡수 (modal 옵션으로) |

**새 폴더**: `components/form/`
**새 id**: `form`
**config**: `FormConfig` (§4.2) — `columns`, `sections`, `saveAction`, `mode: 'inline' | 'modal'`

---

### 3.3 `search` — 검색 필터 (1종)

**통합 대상** (3개 → 1개):

| 기존 ID | 폴더 | 액션 |
|---|---|---|
| `v2-table-search-widget` | `v2-table-search-widget/` | **base** (가장 많이 씀) |
| `table-search-widget` (legacy) | `table-search-widget/` | 삭제 |
| `autocomplete-search-input` (legacy) | `autocomplete-search-input/` | 삭제 (input 그룹에서 autocomplete 옵션으로) |

**새 폴더**: `components/search/`
**새 id**: `search`
**config**: `SearchConfig` (§4.3)

---

### 3.4 `input` — 범용 필드 입력 (1종, 가장 큼)

**핵심**: 하나의 `input` 컴포넌트가 **`FieldConfig.type` 에 따라 완전히 다른 위젯으로 렌더**.

**통합 대상** (20개 이상 → 1개):

| 기존 ID | 역할 | 통합 후 FieldConfig.type |
|---|---|---|
| `v2-input` | text/number/password | `'text'` / `'number'` |
| `v2-select` | dropdown | `'select'` |
| `v2-date` | date picker | `'date'` / `'datetime'` |
| `v2-category-manager` | category select | `'select'` (with cascade) |
| `v2-file-upload` | file attach | `'file'` |
| `v2-media` | image/media | `'file'` (with preview) |
| `v2-numbering-rule` | auto-number | `'code'` |
| `entity-search-input` (legacy) | FK popup | `'entity'` |
| `v2-location-swap-selector` | location picker | `'entity'` (with swap UX) |
| `text-input` (legacy) | | 삭제 |
| `number-input` (legacy) | | 삭제 |
| `date-input` (legacy) | | 삭제 |
| `select-basic` (legacy) | | 삭제 |
| `checkbox-basic` (legacy) | | `'checkbox'` |
| `radio-basic` (legacy) | | `'select'` (렌더 옵션) |
| `toggle-switch` (legacy) | | `'checkbox'` (렌더 옵션) |
| `slider-basic` (legacy) | | `'number'` (렌더 옵션) |
| `textarea-basic` (legacy) | | `'textarea'` |
| `file-upload` (legacy) | | 삭제 |
| `image-display` / `image-widget` (legacy) | | 삭제 |
| `selected-items-detail-input` (legacy) | | 삭제 |
| `mail-recipient-selector` (legacy) | 흡수 검토 | `'entity'` (복수) 또는 도메인 특화 |
| `test-input` (legacy) | | 삭제 (테스트 코드) |

**새 폴더**: `components/input/`
**새 id**: `input`
**규격**: `FieldConfig` 를 직접 소비. `type` 별로 내부 분기 렌더.
**렌더 계약**: `2026-04-08-invyone-component-spec.md` §2.2 매핑 테이블

---

### 3.5 `button` — 액션 버튼 (1종)

**통합 대상** (3개 → 1개):

| 기존 ID | 폴더 | 액션 |
|---|---|---|
| `v2-button-primary` | `v2-button-primary/` | **base** |
| `button-primary` (legacy) | `button-primary/` | 삭제 (legacy) |
| `related-data-buttons` (legacy) | `related-data-buttons/` | 삭제 (버튼 그룹은 button-bar 또는 여러 button 배치) |

**새 폴더**: `components/button/`
**새 id**: `button`
**config**: `ButtonConfig` (§4.4) — `actionType: ActionType` (12종)

---

### 3.6 `stats` — 통계 / KPI 카드 (1종)

**통합 대상** (6개 → 1개):

| 기존 ID | 폴더 | 액션 |
|---|---|---|
| `v2-aggregation-widget` | `v2-aggregation-widget/` | **base** |
| `v2-status-count` | `v2-status-count/` | 흡수 (count 변형) |
| `v2-card-display` | `v2-card-display/` | 흡수 (card 변형) |
| `aggregation-widget` (legacy) | `aggregation-widget/` | 삭제 |
| `card-display` (legacy) | `card-display/` | 삭제 |
| `customer-item-mapping` (legacy) | `customer-item-mapping/` | 삭제 (도메인 특화) |

**새 폴더**: `components/stats/`
**새 id**: `stats`
**config**: `StatsConfig` (§4.6) — `items[]` (label, column, aggregation)

---

### 3.7 `title` — 제목 / 텍스트 (1종)

**통합 대상** (2개 → 1개):

| 기존 ID | 액션 |
|---|---|
| `v2-text-display` | **base** |
| `text-display` (legacy) | 삭제 |

**새 폴더**: `components/title/`
**새 id**: `title`
**config**: `TitleConfig` (§4.6) — `text`, `fontSize`, `fontWeight`, `align`

---

### 3.8 `divider` — 구분선 (1종)

**통합 대상** (3개 → 1개):

| 기존 ID | 액션 |
|---|---|
| `v2-divider-line` | **base** |
| `v2-split-line` | 흡수 (수직/수평 변형) |
| `divider-line` (legacy) | 삭제 |

**새 폴더**: `components/divider/`
**새 id**: `divider`
**config**: `DividerConfig` — `style`, `orientation`

---

### 3.9 `container` — 탭 / 섹션 / 아코디언 / 반복 (1종)

**통합 대상** (11개 → 1개): mockup 의 `LAYOUT` 카테고리 전체

| 기존 ID | 역할 | 통합 후 config |
|---|---|---|
| `v2-tabs-widget` | 탭 | `containerType: 'tabs'` |
| `v2-section-card` | 섹션 카드 | `containerType: 'section'` (card) |
| `v2-section-paper` | 섹션 페이퍼 | `containerType: 'section'` (paper) |
| `v2-repeat-container` | 반복 | `containerType: 'repeater'` |
| `v2-repeater` | 반복 | `containerType: 'repeater'` |
| `accordion-basic` (legacy) | 아코디언 | `containerType: 'accordion'` |
| `section-card` / `section-paper` (legacy) | | 삭제 |
| `tabs` (legacy) | | 삭제 |
| `conditional-container` (legacy) | 조건부 | `containerType: 'conditional'` |
| `repeat-container` / `repeat-screen-modal` / `repeater-field-group` | | 삭제 |
| `screen-split-panel` (legacy) | | 삭제 (table.split 에서 처리) |

**새 폴더**: `components/container/`
**새 id**: `container`
**config**: `ContainerConfig` (신규) — `containerType`, `children: Component[]`

---

## 4. 도메인 특화 — 통합 X, 그대로 유지

다음 컴포넌트는 **업무 특화 로직**이 너무 강해서 통합 불가. 그대로 유지하되 규격 정리(FieldConfig/DataPort 호환)만:

| 컴포넌트 | 도메인 |
|---|---|
| `v2-bom-tree` | BOM |
| `v2-bom-item-editor` | BOM |
| `v2-shipping-plan-editor` | 출하 |
| `v2-timeline-scheduler` | 일정 |
| `v2-process-work-standard` | 공정 표준 |
| `v2-approval-step` | 결재 |
| `v2-rack-structure` | 창고 |
| `v2-item-routing` | 라우팅 |
| `flow-widget` (legacy) | 플로우 |
| `map` (legacy) | 지도 |

이들은 `frontend/lib/registry/components/domain/` 하위로 이주.

---

## 5. 삭제 대상 — 통합도 유지도 X

| 폴더 | 이유 |
|---|---|
| `test-input` | 테스트 코드 |
| `common` | 유틸 (코드만 유지, 컴포넌트 아님 — 삭제 X) |
| `image-display`, `image-widget` | `input` 의 file 타입으로 흡수 |
| `tax-invoice-list` | 도메인 특화 + legacy |

---

## 6. 최종 컴포넌트 구조 (이후)

```
frontend/lib/registry/components/
├── table/                 # 1종 — 데이터 테이블 (9개 흡수)
├── form/                  # 1종 — 입력 폼 (2개 흡수)
├── search/                # 1종 — 검색 필터 (3개 흡수)
├── input/                 # 1종 — 범용 입력 (20+개 흡수, 가장 큼)
├── button/                # 1종 — 액션 버튼 (3개 흡수)
├── stats/                 # 1종 — 통계/KPI (6개 흡수)
├── title/                 # 1종 — 제목/텍스트 (2개 흡수)
├── divider/               # 1종 — 구분선 (3개 흡수)
├── container/             # 1종 — 탭/섹션/아코디언/반복 (11개 흡수)
└── domain/                # 도메인 특화 (그대로 유지, 약 10개)
    ├── bom-tree/
    ├── bom-item-editor/
    ├── shipping-plan-editor/
    ├── timeline-scheduler/
    ├── process-work-standard/
    ├── approval-step/
    ├── rack-structure/
    ├── item-routing/
    ├── flow-widget/
    └── map/
```

**결과**: 86개 → **9 통합 + 10 도메인 = 19개 폴더** (78% 감소)

---

## 7. 통합 작업 순서

### Phase A — 기반 (1단계씩, 작고 안전한 것부터)
1. **`divider`** (3→1) — 가장 간단, 5분 작업
2. **`title`** (2→1) — 간단
3. **`button`** (3→1) — 중간
4. **`search`** (3→1) — 중간

### Phase B — 대규모 (본편)
5. **`input`** (20+→1) — 가장 크고 중요. FieldConfig.type 기반 분기
6. **`stats`** (6→1)
7. **`form`** (2→1)

### Phase C — 가장 복잡
8. **`table`** (9→1) — 가장 많은 기능, displayMode 변형 다수
9. **`container`** (11→1) — 탭/섹션/아코디언/반복

### Phase D — 도메인 정리
10. `domain/` 폴더로 이주 (파일 이동만, 로직 변경 X)

### Phase E — 청소
11. Legacy 폴더 전체 삭제
12. `ComponentRegistry` legacy alias 추가
13. 저장된 화면의 component type 자동 마이그레이션 (`v2-table-list` → `table` 등)

---

## 8. 파일별 액션 매트릭스

| 상태 | 의미 | 개수 |
|---|---|---|
| **base** | 새 통합 폴더의 기준이 되는 원본 (예: v2-table-list → table) | 9개 |
| **흡수** | base 에 기능 병합 후 원본 폴더 삭제 | 약 30개 |
| **삭제** | 완전히 제거 (legacy 또는 중복) | 약 35개 |
| **도메인 유지** | `domain/` 으로 이주만 | 약 10개 |
| **유틸 유지** | `common/` 등 코드 그대로 | 약 2개 |

---

## 9. 리스크 & 완화

### 9.1 기존 저장 화면이 깨질 위험
- DB 에 저장된 화면 JSON 이 `v2-table-list` 같은 구 id 를 참조
- **완화**: `ComponentRegistry` 에 **alias 맵**을 등록해서 구 id 요청 시 새 컴포넌트로 자동 리다이렉트
```ts
const legacyAliases = {
  'v2-table-list': 'table',
  'v2-input': 'input',
  'v2-select': 'input',
  'v2-date': 'input',
  // ...
};
```

### 9.2 기능 누락
- 각 흡수 과정에서 특이 케이스(예: v2-pivot-grid 의 pivot 로직)를 빼먹으면 기능 상실
- **완화**: Phase 단위로 진행 + 각 단계마다 기능 체크리스트 확인

### 9.3 세션 초과
- 한 세션에 전부 못 함. 여러 세션 필요.
- **완화**: Phase 순서대로 진행. 각 Phase 완료 시 커밋.

### 9.4 롤백
- 각 Phase 시작 전 git commit. 문제 생기면 되돌림.
- Legacy 폴더는 바로 삭제 X → `_obsolete/legacy/<name>/` 로 이동 후 최종 청소 단계에서 삭제

---

## 10. 검증 체크리스트 (Phase 별)

### 각 Phase 완료 시
- [ ] 새 통합 폴더가 생성됨
- [ ] 새 컴포넌트가 `ComponentRegistry` 에 등록됨
- [ ] 기존 폴더가 `_obsolete/` 로 이동됨
- [ ] 팔레트에서 새 컴포넌트가 보임
- [ ] 기존에 저장된 화면이 **alias 를 통해** 정상 렌더됨
- [ ] `npx tsc --noEmit` 통과 (해당 영역 에러 0개)
- [ ] V2PropertiesPanel 에서 새 컴포넌트 config 편집 가능

### 전체 완료 시
- [ ] `components/` 폴더에 9 통합 + `domain/` + `common/` 만 남음
- [ ] 모든 컴포넌트가 `FieldConfig` / `DataPort` / `ComponentTypeConfig` 규격 준수
- [ ] 기존 VEX 화면 80개 이상 샘플이 정상 동작
- [ ] Legacy 폴더 완전 삭제

---

## 10.5. ★ Phase 마다 반드시 할 일 (체크리스트)

Phase A-1/A-2 에서 발견된 이슈들. 각 Phase 에서 통합할 때 **반드시**:

1. **새 통합 폴더 생성 + 파일 작성** (types, Component, ConfigPanel, Renderer, index)
2. **`frontend/lib/registry/components/index.ts` 에 새 Renderer import 추가**
3. **기존 흡수 대상 `index.ts` 에 `hidden: true` 추가** (미래 대비)
4. **`ComponentsPanel.tsx:107` 의 `hiddenComponents` 배열에 흡수 대상 id 전부 추가** ⚠️
   - 이유: `ComponentsPanel` 이 `ComponentDefinition.hidden` 필드를 **무시**하고
     이 배열로 필터함
5. **`lib/utils/getComponentConfigPanel.tsx:9` 의 `CONFIG_PANEL_MAP` 에 새 컴포넌트
   등록** ⚠️
   - 이유: V2PropertiesPanel 이 `ComponentDefinition.config_panel` 을 **무시**하고
     이 하드코딩 맵으로 ConfigPanel 을 찾음
   - 패턴: `"<new-id>": () => import("@/lib/registry/components/<new-folder>/<Name>ConfigPanel")`
6. **타입 체크**: `npx tsc --noEmit 2>&1 | grep <new-folder>`
7. **브라우저 확인**: 검색 → 1개만, 드롭 → 성공, 선택 → **우측 속성 패널에 ConfigPanel 뜸**

### ★ 드래그 드롭 기존 버그 (2026-04-11 발견, 수정 완료)

`ScreenDesigner.handleComponentDrop` 이 `component.defaultSize` (camelCase) 로
접근하지만 `ComponentDefinition` 은 `default_size` (snake_case) 로 저장. 격자
스냅이 켜진 상태에서 TypeError 로 drop 이 조용히 중단됨.

**픽스**: `ComponentsPanel.handleDragStart` 가 drag data 에 camelCase 별칭
(`defaultSize`, `defaultConfig`, `webType`) 을 주입해서 호환.

### ★ 컴포넌트 설정 저장·전달 경로 (Phase A-2 발견, 4단계 머지 필요)

layout 의 각 컴포넌트는 설정을 **`componentConfig`** 필드에 저장한다
(`component.config` 아님). 그리고 렌더 시점에 `DynamicComponentRenderer.tsx:882`
가 이걸 **top-level props 로 spread** 해서 전달하므로, 실제로는 4가지 경로가
동시에 존재한다:

1. `props.config` — `DynamicComponentRenderer` 가 명시적으로 주입
2. `props.componentConfig` — 같은 값을 다른 이름으로 주입
3. `props.component.componentConfig` — component 객체 안의 실제 저장 경로
4. **`props.*` (top-level spread)** — `mergedComponentConfig` 를 `{...}` spread
   로 펼쳐서 각 필드가 직접 props 에 들어감 (text, color, thickness 등)

또한 일부 경로에서 `component.config` 를 legacy 로 쓰는 경우가 있음.

**새 통합 컴포넌트의 렌더 함수 (XxxComponent.tsx) 는 반드시**:

```tsx
// 1) 먼저 top-level spread 된 config 필드들을 props 에서 추출
const fromProps: Partial<XxxConfig> = {};
const p = props as any;
if (p.text !== undefined) fromProps.text = p.text;
if (p.color !== undefined) fromProps.color = p.color;
// ... 각 config 필드 추출

// 2) 4경로 머지 (마지막이 우선순위 최상위)
const componentConfig = {
  ...config,
  ...((component as any).config ?? {}),          // legacy
  ...((component as any).componentConfig ?? {}), // 저장 경로
  ...fromProps,                                  // ★ top-level spread (최우선)
} as XxxConfig;
```

`fromProps` 가 빠지면 ConfigPanel 변경이 간헐적으로 또는 전혀 반영 안 됨.
(Phase A-2 에서 title / divider 가 이 증상 보임)

### ★ DynamicComponentRenderer 의 componentConfig 읽기 버그 (Phase A-2 결정타)

`DynamicComponentRenderer.tsx:848` 이 **`component.component_config`** (snake_case)
만 읽고 있었는데, `V2PropertiesPanel.onUpdateProperty(id, "componentConfig", ...)`
는 **`componentConfig`** (camelCase) 에 저장. 이 불일치 때문에 **설정 변경이 리렌더
때 사라지고 있었음**.

기존 v2-* 컴포넌트는 `mergeColumnMeta` 가 DB 컬럼 메타를 자동 채워줘서 이 버그가
숨겨져 있었음. **컬럼 없는 일반 컴포넌트(divider/title)** 만 정면으로 영향.

**픽스** (2026-04-11):
```ts
const savedConfig =
  (component as any).componentConfig ||  // camelCase (저장 경로)
  component.component_config ||           // snake_case (legacy fallback)
  {};
```

### ★ DynamicComponentRenderer 자동 v2- 매핑 함정 (Phase B-1 결정타)

`DynamicComponentRenderer.tsx:351` 의 `mapToV2ComponentType` 은 componentType
에 **자동으로 `v2-` 접두사를 붙여서 resolve 시도**:

```ts
const v2Type = `v2-${type}`;
if (ComponentRegistry.hasComponent(v2Type)) {
  return v2Type;  // ← 자동 리다이렉트
}
```

즉 내가 `input` 이라는 새 통합 컴포넌트를 만들어도, 팔레트에서 드롭된 블록의
`componentType='input'` 이 **`v2-input` 으로 리다이렉트**되어 기존 v2-input
컴포넌트가 렌더된다. 내 InputComponent 는 호출조차 안 됨.

Phase A-1~A-4 는 우연히 피했음:
- `divider` → `v2-divider` 없음 (v2-divider-line 은 다른 이름)
- `title` → `v2-title` 없음 (v2-text-display 는 다른 이름)
- `button` → `v2-button` 없음 (v2-button-primary)
- `search` → `v2-search` 없음 (v2-table-search-widget)

**B-1 input 만 정면으로 걸림**. `v2-input` 이 존재하므로 자동 매핑됨.

**픽스** (2026-04-11):
```ts
const INVYONE_UNIFIED_IDS = new Set([
  "divider", "title", "button", "search",
  "input", "stats", "form", "table", "container",
]);

const mapToV2ComponentType = (type) => {
  if (INVYONE_UNIFIED_IDS.has(type)) return type; // ★ 매핑 제외
  // ...
};
```

**Phase 마다 체크리스트에 추가**: 새 통합 컴포넌트 id 를 이 Set 에 추가.

### 새 통합 컴포넌트 표준 패턴 (Phase A-3 부터)

모든 새 통합 컴포넌트의 `XxxComponent.tsx` 는 다음 패턴을 **처음부터** 따를 것:

1. **fromProps 추출**: top-level spread 된 config 필드를 `props` 에서 추출
2. **4경로 머지**: `{ ...config, ...component.config, ...component.componentConfig, ...fromProps }`
3. **DOM props filter**: config 필드들을 `...domProps` 에 남기지 말고 destructure 로
   제거 (React warning 방지)

이 3가지 빠지면 설정이 반영 안 되거나 콘솔 에러 폭주.

### 중장기 TODO (청소 단계 Phase E 에서)

- `ComponentsPanel` 필터를 `!c.hidden` 으로 리팩토링해서 `hiddenComponents` 배열 제거
- `getComponentConfigPanel` 을 `ComponentDefinition.config_panel` 직접 사용으로 리팩토링
- `handleComponentDrop` 을 `default_size` (snake_case) 로 통일
- 그러면 위 체크리스트 4번, 5번 단계가 자동화됨

---

## 11. 다음 세션 작업 목록

1. **Phase A-1**: `divider` 통합 (3→1) — 워밍업
2. **Phase A-2**: `title` 통합 (2→1)
3. **Phase A-3**: `button` 통합 (3→1)
4. **Phase A-4**: `search` 통합 (3→1)
5. Phase A 완료 → 커밋 → Phase B 시작

각 Phase 는 별도 커밋. 각 커밋 메시지 예:
```
refactor(components): unify divider group (3→1)

- v2-divider-line, v2-split-line, divider-line → components/divider/
- legacy aliases registered
```

---

## 끝

이 문서가 86→9 통합의 **진실의 원천**. 이후 세션에서 작업 시작 시 이 문서 참조.
변경이 생기면 이 문서를 먼저 수정.
