# INVYONE Input 통합 — Canonical 마이그레이션 진행 노트

날짜: 2026-05-07 ~ 2026-05-11
작업자: gbpark
컨텍스트: 인비원스튜디오의 입력 계열을 **FieldConfig / DataPort 계약을 지키는 canonical `input`** 으로 통합 중. 설정 패널은 계약을 편집하는 UI일 뿐, 진실의 원천은 `frontend/types/invyone-component.ts` 의 FieldConfig / DataPort.

---

## 0. 핵심 원칙

- INVYONE = VEX 의 2세대 리뉴얼. **운영 단계 아님** → 옛 키 fallback / 호환 부담 X. 깨끗한 canonical 1안.
- FieldConfig 가 유일한 필드 규격이고, DataPort / Connection 이 컴포넌트 통신 계약.
- 옛것이 남아있으면 통합 아님. V2 입력/선택은 구현체·alias·fallback·DB 마이그 대상이 아니라 제거 대상.
- GPT-5.5 (codex:rescue) 와 단계마다 교차 검증.

## 1. 큰 그림 (목표 형태)

```
[8 통합 컴포넌트] = [FieldConfig/DataPort 계약을 소비하는 단일 캔버스 컴포넌트]
  input    → FieldConfig + DataPort + InputComponent  (text/number/money/date/single/multi/autonum/formula/audit/file)
  table    → InvTableConfigPanel
  search · button · title · divider · stats · container

[옛 V2 / 옛 6개 컴포넌트] → 기능 이식 후 폐기
  옛 입력/선택 본체 2개 — 삭제 완료, 필요한 기능은 canonical input 으로 흡수
  date-input / text-input / number-input / select-basic / checkbox-basic / textarea-basic 의 자체 캔버스 컴포넌트 6개
```

---

## 2. 완료 작업

### Phase 1 — ConfigPanel 통합 (이전 세션 + 일부 본 세션)

- ✅ `InvFieldConfigPanel` 의 brumb (4 kinds × 10 types × 32 formats) 그대로 canonical
- ✅ `resolveTriple` 에 옛 6개 컴포넌트 ID → triple default 분기 추가
- ✅ 옛 6개 `index.ts` — config_panel: InvFieldConfigPanel + default_config 에 `{kind, type, format}` triple 추가
- ✅ 신규 `input/index.ts` — config_panel: InvFieldConfigPanel + default_config triple
- ✅ `CONFIG_PANEL_MAP["input"]` → InvFieldConfigPanel

### Phase 2 — applyTriple canonical cleanup

- ✅ `TYPE_VOLATILE_FIELDS` 상수 + `clearVolatileFields` 함수
- ✅ text/number/money/date/choice 분기 자기 필드만 set, 잔재 일괄 reset
- ✅ formula `next.computed = prev.computed || ""` 패치 (분기 순환 시 사용자 수식 보존)
- ✅ auto/attach 분기 redundant `next.source = undefined` 제거

### Phase 3 — 캔버스 라우팅 통일

- ✅ `DynamicComponentRenderer.tsx:418-453` 의 fieldType / dbInputType → v2-input/v2-select **강제 swap 분기 통째 제거**
- ✅ `webTypeMapping.ts` 의 text 계열 9개 (text/email/password/tel/url/textarea/number/decimal/label) → `input`
- ✅ 초기 fallback 도 `input` canonical 로 정리했고, Phase D.2 에서 V2 입력/선택 fallback 자체 제거

### Phase 4 — InputComponent 외형 통일

- ✅ container 가 input box 역할 (border + radius + bg, padding 0)
- ✅ `baseInputStyle` 에서 자체 border 제거 + transparent (이중 박스 해소)
- ✅ `inputSlotStyle` (flex:1, width/height 100%) wrapper — 모든 type 의 위젯 박스 가득
- ✅ label position: absolute, top: -18 (박스 바깥 위 — V2 스타일)
- ✅ entity outer div / button height 100% + flexShrink:0
- ✅ text 분기에서 format 별 native input type 분기 (password/email/tel/url)

### Phase 5 — pickers 통일 (date 계열)

- ✅ `SingleDatePicker` / `DateTimePicker` / `TimePicker` / `RangeDatePicker` className prop 전파 → 자체 border 제거
- ✅ `DateTimePicker` 의 sub-picker (SingleDatePicker + TimePicker) 에 className 전파
- ✅ `DateTimePicker` gap-2 → 0 + 가운데 1px divider
- ✅ `TimePicker` 의 shadcn `Input` → raw `<input>` (default class 회피)
- ✅ `RangeCalendarPopover` className prop 받음 + RangeDatePicker 가 sub 에 전달
- ✅ `RangeDatePicker` gap-2 → 0

### Phase A.5 — 자동생성 hook

- ✅ `InputComponent` 에 useEffect — `autoGeneration.enabled` 시 `AutoGenerationUtils.generateValue` 호출
- ✅ 조건: 디자인 모드 X + 값 비어있을 때만 trigger
- ✅ 처리 type: uuid / current_user / current_time / sequence / random_string / random_number / company_code / department
- ✅ `numbering_rule` 은 별도 (Phase A.6 에서)

### Phase B.1 — select-pickers 모듈 시작

- ✅ `frontend/lib/registry/components/input/select-pickers.tsx` 신규
- ✅ `SingleSelectPicker` — Custom Popover dropdown + 검색 + allowClear + 외부 클릭 닫기 + ESC
- ✅ InputComponent select 분기 → SingleSelectPicker 사용 (native `<select>` 대체)
- ✅ webTypeMapping `select / dropdown` → `input` (single dropdown)

### Phase B.2 — MultiSelectPicker

- ✅ `MultiSelectPicker` 신규 (select-pickers.tsx) — 체크박스 list + maxSelect 차단 + 라벨 join 트리거
- ✅ InputComponent select 분기에 multi 분기 추가 (`type=multi` 또는 `config.multiple` 시)
- ✅ value 정규화 (string / string[] 둘 다 받음)

### Phase B.4 — displayMode (mode) 통합

- ✅ 처음 시도: `displayMode` 신규 prop + UI — **중복 발견 후 폐기** (기존 `config.mode` 와 동일)
- ✅ 정정: 기존 `config.mode` 사용 (dropdown/combobox/radio/check/tag/toggle 6 가지)
- ✅ `RadioPicker` 신규 — single + mode=radio (라디오 button list)
- ✅ `CheckboxListPicker` 신규 — multi + mode=check (체크박스 list, maxSelect 차단)
- ✅ InputComponent select 분기 — mode 따라 4 picker 분기 (Single/Multi/Radio/CheckboxList)
- ✅ ConfigPanel "선택 방식" 옵션을 multi prop 따라 분기 — single: dropdown/combobox/radio/toggle, multi: dropdown/combobox/check/swap/tag
- ✅ TYPE_VOLATILE_FIELDS 에 `maxSelect` 추가 (`mode` 는 기존)

### Phase B.4 추가 정리 — 외각 box + 복수 선택 토글

- ✅ picker 4개 wrapper 의 `opacity-50` 제거 (시각 흐릿 해소)
- ✅ InputComponent select 분기 disabled 에 `isDesignMode` 빼기 — 디자인 모드에서도 클릭 가능 (시각만)
- ✅ container border/bg 분기 — `mode === "radio" || "check"` 시 외각 box 제거 (자체 visual element 가 표시)
- ✅ 고급 설정의 "복수 선택" CPSwitch 제거 — brumb 의 `단일/다중` 이 진실의 원천 (단일/다중 = 저장 형태 차이, UX 토글 아님)
- ✅ "최대 개수" 노출 조건 — `multi prop` 으로 분기 (config.multiple 의존 X)
- ✅ input default_size 높이 48 → 30 (사용자 의도)

### Phase B.4 의 알려진 이슈 (이후 해결됨)

- ✅ ~~기본 선택값 (`config.defaultValue`) 동작 안 함~~ — 2026-05-11 해결 (BlockRenderer hijack 버그)
- ⚠️ dropdown 모드인데 박스 없음 사례 (사진 #32). 원인 가설: `config.mode` 잔재 ("radio"/"check"). 새 컴포넌트 끌어 놓으면 박스 정상

---

## 2026-05-11 진행 (이번 세션)

### Phase C 보정 — entity 는 code-name 옵션 source (2026-05-12)

**배경**: 1차 C에서 `format=entity` 를 검색 모달로 해석했으나 사용자 점검으로 계약 오류 확인.
INVYONE input 의 entity 는 DB FK 를 직접 걸 수 없는 경우 화면 설정에서 참조 테이블과
저장 값/표시 라벨 컬럼을 지정해 code-name 옵션을 가져오는 선택형이다.
검색 모달은 별도 `entity-search-input` 계열의 책임이지 canonical input entity 의 기본 동작이 아니다.

**변경**:
- ✅ `useOptionLoader` — `source=entity` 를 `/entity/{entityTable}/options?value={entityValueColumn}&label={entityLabelColumn}` 로 로드
  - `config.ref.table/valueColumn/displayColumn` 도 같은 계약으로 처리
- ✅ `InputComponent` — `type=single|multi + format=entity` 는 select 계열 picker 로 렌더
  - 단일 entity: `SingleSelectPicker` (검색 가능 dropdown)
  - 다중 entity: `MultiSelectPicker` / `mode=check` / `mode=swap` 지원
- ✅ 잘못 추가된 canonical input 전용 `EntityPicker` 모달 경로 제거
- ✅ `V2PropertiesPanel` / `InvFieldConfigPanel` — 참조 테이블 목록에서
  `table_name/display_name` 과 `tableName/displayName` 을 모두 정규화

**미구현 (후속)**:
- 옵션 필터 (`filters`) 의 runtime query 전달

### Phase C.2 — v2-input/v2-select 재유입 차단 (2026-05-12)

**배경**: 목표는 V2 입력/선택 컴포넌트를 계속 쓰는 게 아니라, 필요한 기능만 `input` canonical 로 흡수하는 것. 그런데 webType 매핑과 renderer 우회로가 여전히 `v2-select` 를 생성/호출하고 있었음.

**변경**:
- ✅ `webTypeMapping.ts` — radio/checkbox/boolean/code/entity/category 모두 `componentType: "input"` 으로 변경
- ✅ `DynamicComponentRenderer.tsx` — category 고급 모드에서 `V2SelectRenderer` 를 직접 require 하던 분기 제거
- ✅ `components/index.ts` — `v2-input/V2InputRenderer`, `v2-select/V2SelectRenderer` auto-register import 제거
- ✅ `registerV2Components.ts` — `v2-input`, `v2-select` 레지스트리 등록 제거
- ✅ `InputComponent.tsx` — `format="entity"` 는 select 계열 picker 로 라우팅
- ✅ `useOptionLoader.ts` — `entityTable/entityValueColumn/entityLabelColumn` 로 code-name options 로드
- ✅ `ScreenSettingModal.tsx` — 폼 필드 자동 추가 시 `text-input` 대신 `input` canonical 생성

**검증**:
- `git diff --check` 통과
- `npx tsc --noEmit --pretty false` 는 기존 전역 타입 오류로 실패. 변경 파일 필터 기준 신규 오류 없음.

### Phase C — entity 선택형 보정 (2026-05-12 완료)

**배경**: input entity 는 마스터 데이터 (거래처/품목/사원) 의 참조 테이블에서 저장 값(code)과
표시 라벨(name)을 지정해 옵션으로 가져오는 기능. DB FK 강제 대신 화면 레벨에서 code-name 참조를 건다.

**변경**:
- ✅ `InputComponent.tsx` — case "entity" 는 모달이 아니라 `SingleSelectPicker` / `MultiSelectPicker`
- ✅ `useOptionLoader.ts` — entity source option loading 추가
- ✅ `frontend/lib/registry/components/input/entity-picker.tsx` 삭제 — canonical input 에 모달 검색 재유입 방지

**관련 파일 맵 업데이트**:
- canonical: `frontend/lib/registry/components/input/use-option-loader.ts` 의 `source=entity`
- 별도 검색 UI: `lib/registry/components/entity-search-input/` 는 명시 검색 컴포넌트가 필요할 때만 사용

### Phase C.3 — entity code-name 안정화 (2026-05-12)

**배경**: 1차 C 후 사용자 점검 — InvFieldConfigPanel 의 entity 설정 UI 에서 참조 테이블 dropdown 이
가끔 비고, value/label 컬럼을 바꾼 뒤에도 옵션이 갱신되지 않는 케이스 확인. 검색 모달 재유입은
없어야 한다는 계약은 그대로 유지.

**점검 결과 — 이미 정상**:
- `V2PropertiesPanel.normalizeConfigPanelTables` — `tableName/displayName` 과 `table_name/display_name` 모두 흡수.
  InvFieldConfigPanel 에 `tables` / `allTables` 정규화된 값 전달
- `InvFieldConfigPanel.normalizeTableSelectOptions` — 동일하게 양쪽 키 흡수. EntityOptions 호출 시
  `allTables.length > 0 ? allTables : tables` 폴백
- `InvFieldConfigPanel.loadColumnsForTable` — `columnName/column_name` + `displayName/display_name` 흡수
- `InputComponent.tsx` case "entity" — `SingleSelectPicker` / `MultiSelectPicker` / `CheckboxListPicker` /
  `SwapPicker` 로만 라우팅. 모달 / EntityPicker import 없음
- `useOptionLoader.ts` source=entity — `/entity/{entityTable}/options?value={...}&label={...}` 로 fetch.
  응답 shape (`data[]`, `data.success+data`, `data.options`) 정상 normalize

**수정 — fetchUrl dep 누락 (옵션 갱신 안 되는 진짜 원인)**:
- `use-option-loader.ts` 의 `fetchUrl` useMemo 의존성 배열에 다음 6개 추가:
  - `config.entityTable` / `config.entityValueColumn` / `config.entityLabelColumn`
  - `config.ref?.table` / `config.ref?.valueColumn` / `config.ref?.displayColumn`
- ref 객체 자체를 dep 으로 넣지 않은 이유: 객체 reference 변동만으로 effect 폭주를 방지하기 위해
  내부 키만 추출

**디자인 모드 가드**:
- `useOptionLoader` 는 `isDesignMode === true` 시 `needsFetch = false` 로 fetch 자체 skip
- `InvFieldConfigPanel` 의 테이블/컬럼 목록 로드는 별도 경로 (apiClient 직접 호출, isDesignMode 무관)
  → 설정 패널 동작은 그대로 허용

**canonical input 에 모달 재유입 없음 (확인)**:
- `rg "EntityPicker|entity-picker|EntitySearchModal" frontend/lib/registry/components/input frontend/components/v2/config-panels/InvFieldConfigPanel.tsx` → 0건

### Phase C.4 — option filters runtime 적용 (2026-05-12)

**배경**: `OptionFilter[]` 가 InvFieldConfigPanel 에 저장만 되고 runtime 에 반영되지 않았음.
canonical input 의 entity / distinct / db source 에서 filter 를 실제 query 로 전달해 결과 옵션을 좁힌다.
검색 모달 재유입 / v2-input·v2-select 재도입 / FieldConfig·DataPort 축소는 모두 금지.

**변경**:
- ✅ `use-option-loader.ts` — `OptionLoaderConfig.filters?: OptionFilter[]` 추가
- ✅ `OptionUserContext` 타입 신규 — `companyCode / userId / deptCode / userName`
- ✅ `UseOptionLoaderArgs.userContext` 추가 — `value_type === "user"` 필터 치환용
- ✅ `resolveFilters(filters, formData, userContext)` 헬퍼 — 치환 규칙:
  - `column` trim 후 빈 값 → skip
  - `value_type=static` → `f.value` 그대로
  - `value_type=field` → `formData[field_ref]`, 값 없으면 skip
  - `value_type=user` → `userContext[user_field]`, 값 없으면 skip
  - `isNull / isNotNull` → value 없이 통과
  - `in / notIn` → 배열 또는 `"a,b,c"` 문자열 모두 trim 된 배열로 정규화
- ✅ `resolvedFiltersJson` useMemo — runtime 치환 결과를 `JSON.stringify` 로 stable string 화.
  결과가 빈 배열이면 `undefined` (URL 에 query 자체 미추가)
- ✅ `fetchUrl` useMemo — `filters=` URL-encoded JSON 으로 append:
  - **entity**: `/entity/{table}/options?value={v}&label={l}&filters={...}` (1순위)
  - **distinct / select**: `/entity/{table}/distinct/{column}?filters={...}`
  - **db (legacy)**: `/entity/{table}/options?value={v}&label={l}&filters={...}`
  - **code / category / api**: 백엔드 endpoint 가 filter 스펙을 정의하지 않아 미적용 (TODO 주석 명시)
- ✅ `fetchUrl` dep 배열에 `resolvedFiltersJson` 추가 → filter 값 변경 시 URL 재생성 → cache key 자동 신규화
- ✅ `resolvedFiltersJson` 자체의 dep — `config.filters`, `formData`, `userContext` 4 키 (객체 reference 폭주 방지)
- ✅ `InputComponent.tsx` — props 에서 `companyCode / user_id / dept_code / user_name` 등 camel/snake 모두 흡수해
  `userContext` 객체 구성 후 `useOptionLoader` 에 전달. DOM noise 누설 방지를 위해 destructure 목록에 4개 추가

**filter 치환 예시 — entity URL**:
```
config.entityTable = "tb_customer";
config.entityValueColumn = "customer_code";
config.entityLabelColumn = "customer_name";
config.filters = [
  { column: "is_active", operator: "=", value_type: "static", value: "Y" },
  { column: "company_code", operator: "=", value_type: "user", user_field: "companyCode" },
];
// runtime 사용자 companyCode = "C0001"

→ /entity/tb_customer/options?value=customer_code&label=customer_name
   &filters=%5B%7B%22column%22%3A%22is_active%22%2C%22operator%22%3A%22%3D%22%2C%22value%22%3A%22Y%22%7D%2C%7B%22column%22%3A%22company_code%22%2C%22operator%22%3A%22%3D%22%2C%22value%22%3A%22C0001%22%7D%5D
```

**cache / race / design mode 영향**:
- **cache**: 기존 `responseCache: Map<url, options>`. filters 값이 바뀌면 URL 이 신규 → cache key 자동 신규.
  이전 결과는 그대로 보존되어 같은 필터로 돌아오면 즉시 hit
- **race**: 기존 `reqIdRef` + `cancelled` flag 유지. URL 변경 시 in-flight 요청 결과 무시
- **design mode**: 기존 `isDesignMode === true` 면 `needsFetch=false` 로 fetch skip 흐름 그대로. filter resolver
  는 dep 평가 단계에서만 도는 정도이고 API 호출 자체가 없음

**InvFieldConfigPanel UI**: 이미 filters 입력 UI 존재. 이번 단계는 runtime 전달만 보완, UI/문구 변경 없음.

**검증**:
- `git diff --check` — whitespace 0
- `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer" frontend/lib frontend/components frontend/app frontend/types frontend/styles` — 0건
- `rg "EntityPicker|entity-picker|EntitySearchModal" frontend/lib/registry/components/input frontend/components/v2/config-panels/InvFieldConfigPanel.tsx` — 0건
- `tsc` 변경 파일 (`use-option-loader`, `InputComponent`) 신규 오류 0건

### Phase D.4 — file / image / img → canonical input 통합 (2026-05-12)

**배경**: webTypeMapping 의 `file / image / img` 와 DynamicWebTypeRenderer 의 강제 분기가
여전히 `v2-file-upload / FileUploadComponent / ImageWidget` 로 빠지고 있었음. canonical input
에 file 분기는 기존부터 있었지만 native `<input type=file>` 단독이라 미리보기 / 다중 칩 / 삭제
UX 가 빠져 있었음. 옛 본체 파일 삭제는 사용처 0건 확인 후 별도 phase.

**변경**:
- ✅ `frontend/lib/registry/components/input/file-picker.tsx` 신규 — `FilePicker`
  - props: `value / onChange / accept / multiple / maxFiles / disabled / readonly / placeholder / showPreview / className`
  - 값 형태: `File | string | Array<File | string> | null` (서버 저장된 path 문자열도 흡수)
  - 이미지면 `URL.createObjectURL` preview thumbnail (cleanup 효과 포함, 메모리 누수 방지)
  - SSR/Node 경로에서 `File` 전역이 없을 수 있어 `typeof File !== "undefined"` 가드 적용
  - 그 외 파일은 `FileText` 아이콘 + 파일명 (max-w-160 truncate + tooltip)
  - 다중 시 `maxFiles` 초과 자르기 + 한도 도달 시 `+추가` 버튼 disabled
  - `disabled / readonly` → `lockEdit` 통합 (선택/삭제/추가 모두 차단)
  - 같은 파일 재선택 가능하도록 hidden input value 매번 초기화
- ✅ `frontend/lib/registry/components/input/types.ts` — `InputConfig` 에 `maxFiles / showPreview` 추가
- ✅ `frontend/lib/registry/components/input/InputComponent.tsx` — case "file"
  - native `<input type=file>` 단독 → **FilePicker** 호출
  - `format === "image"` 면 자동 `accept="image/*"` + `showPreview=true`
  - `format === "doc"` 면 `.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx`
  - 사용자 명시 `accept / showPreview` 가 있으면 그 값 우선
  - fallback webType 에 `img / picture / photo` 도 file 로 흡수
- ✅ `frontend/lib/utils/webTypeMapping.ts`
  - `file / image / img` 의 `WEB_TYPE_V2_MAPPING` 항목 → `componentType: "input"` 으로 변경
  - config triple: `{ kind: "attach", type: "file", format: "file" | "image", accept, multiple, maxFiles, showPreview }`
  - `WEB_TYPE_COMPONENT_MAPPING` 도 `file/image/img → "input"`
- ✅ `frontend/lib/registry/DynamicWebTypeRenderer.tsx`
  - `webType === "file" / "image" / "img" / "picture" / "photo"` 와 `props.component?.type === "file"` 모두
    canonical `InputComponent` 로 라우팅 (file config triple 주입). 옛 `FileUploadComponent` / `ImageWidget`
    직접 import 분기 3곳 제거 (참조만 끊고 파일 자체는 잔존 — 별도 phase)
  - 기존 타입 mismatch 보정: `webType/web_type` 양쪽 prop 흡수, `defaultConfig` → `default_config`
- ✅ `frontend/components/screen/panels/ComponentsPanel.tsx` — hidden 목록 주석 갱신
  - "→ V2Media" / "→ v2-media (image)" / "통합 미디어" 등을 "→ canonical input (type='file', format='image')" 로 우회
  - `v2-media` / `v2-file-upload` 두 항목은 hidden 유지 (생성 차단)
- ✅ `notes/...` 본 노트 — Phase D.4 기록

**file / image / img 가 이제 어떤 config 로 input 생성되는지 예시**:

```jsonc
// webType = "file"
{
  "componentType": "input",
  "config": {
    "kind": "attach",
    "type": "file",
    "format": "file",
    "accept": "*/*",
    "multiple": true,
    "maxFiles": 10
  }
}

// webType = "image" 또는 "img"
{
  "componentType": "input",
  "config": {
    "kind": "attach",
    "type": "file",
    "format": "image",
    "accept": "image/*",
    "multiple": false,
    "maxFiles": 1,
    "showPreview": true
  }
}
```

**FieldConfig / DataPort 영향**: **0건**
- `frontend/types/invyone-component.ts` 의 FieldType / FieldConfig / DataPort / Connection 변경 없음
- `frontend/lib/dataPort/runtime.ts` 변경 없음
- 추가된 `InputConfig.maxFiles` / `InputConfig.showPreview` 는 input 컴포넌트 내부 옵션 (FieldConfig 와는 별개 — InputComponent 의 InvField triple 흡수 후 props 분리)

**남은 old file/media 삭제 후보 (다음 phase)**:
- `frontend/lib/registry/components/file-upload/FileUploadComponent.tsx` + `index.ts` + `FileUploadConfigPanel.tsx`
- `frontend/lib/registry/components/v2-file-upload/` 폴더 통째
- `frontend/lib/registry/components/v2-media/` 폴더 통째 (`V2MediaRenderer` + config 패널)
- `frontend/components/v2/V2Media.tsx` 본체 + V2Media 관련 타입 / 데모 / 매핑
- `frontend/lib/registry/components/image-widget/` + `image-display/`
- `frontend/components/screen/widgets/types/ImageWidget.tsx`
- **사전 확인 필요**: 위 컴포넌트들이 외부에서 import 되는 경로 grep 후 0건일 때만 삭제

**검증**:
- ✅ `git diff --check` — whitespace 0
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer"` — 0건 유지
- ✅ `rg "componentType: \"v2-file-upload\"|file: \"v2-file-upload\"|image: \"v2-file-upload\"|img: \"v2-file-upload\""` — 새 생성/매핑 경로 **0건**
- ✅ `rg "EntityPicker|entity-picker|EntitySearchModal" canonical input 경로` — 0건 유지
- ✅ `tsc` 변경 파일 신규 오류 0건 (`DynamicWebTypeRenderer` 의 기존 `webType`/`defaultConfig` mismatch 도 함께 정리)

### Phase D.5 — canonical file 저장 파이프라인 + old file/media 런타임 차단 (2026-05-12)

**배경**: Phase D.4 에서 file/image/img 새 생성 경로는 canonical input 으로 통합. 본 phase 는
(1) canonical input file 값이 master save 흐름에서 누락되지 않게 보완 + (2) 옛 file-upload /
image-widget / image-display / v2-media / v2-file-upload 의 런타임 / registry / ConfigPanel
경로 차단. 옛 본체 파일 자체 삭제는 외부 import 정밀 분석 후 별도 phase.

**canonical file 저장값 변환 정책 (master save)**:
- `InteractiveScreenViewerDynamic.handleSaveAction` 의 `masterFormData` 구성 단계 보강
- 파일 컬럼 식별 (`isFileColumnComponent`):
  - 옛: `componentType ∈ {v2-media, file-upload}` 또는 `url.includes(...)` (호환용)
  - canonical: `componentType === "input"` && (`config.type === "file"` || `config.kind === "attach"` || `config.format ∈ {file, image, doc}`)
  - 보정: `componentConfig / config / overrides` 세 저장 위치를 모두 흡수. 컬럼명도 `column_name / columnName` 양쪽 키를 같은 우선순위로 확인.
- 값 별 처리:
  - `null / undefined` → 그대로
  - `string / string[]` → 이미 저장된 path/id 로 보고 유지 (업로드 skip)
  - `File / File[]` → `uploadFiles` (POST `/files/upload`) 호출 → 응답 `FileInfo[].id ?? server_path ?? server_filename` 로 치환
  - 혼합 (`(File | string)[]`) → string 유지 + File 만 업로드 후 합쳐서 배열로
  - 업로드 실패 시 안전하게 string 만 유지 (브라우저 `File` 객체가 saveData 에 들어가지 않도록)
- 파일 컬럼이 아닌 배열 값은 종전대로 repeater 로 보고 제외
- 파일 컬럼은 더 이상 repeater 오인 제외 대상이 아님

**제거한 old runtime / register / config 경로**:
- `InteractiveScreenViewerDynamic.tsx` — `FileUploadComponent` import 제거, `isFileComponent` import 제거,
  `if (isFileComponent(comp)) return renderFileComponent(comp)` 분기 제거, `renderFileComponent` 함수 통째 제거,
  `uploadFilesAndCreateData` 직접 import 제거 (`uploadFiles` 동적 import 가 master save 안에서 대체)
- `lib/registry/components/index.ts` — `file-upload/FileUploadRenderer`, `image-widget/ImageWidgetRenderer`,
  `image-display/ImageDisplayRenderer`, `v2-media/V2MediaRenderer`, `v2-file-upload/V2FileUploadRenderer`
  auto-register import 5개 모두 제거
- `lib/utils/getComponentConfigPanel.tsx` — `CONFIG_PANEL_MAP` 의 `v2-media / file-upload / image-display / image-widget` mapping 4개 제거
- `components/screen/panels/V2PropertiesPanel.tsx` — `v2ConfigPanels` 의 `v2-media` 매핑 제거
- `lib/schemas/componentConfig.ts` — `v2MediaOverridesSchema` 정의 + `componentOverridesRegistry` 의 `v2-media` 등록 + `componentDefaultsRegistry` 의 `v2-media` 기본값 모두 제거
- `components/v2/registerV2Components.ts` — `V2Media` / `V2MediaConfigPanel` import + v2-media `ComponentDefinition` 등록 제거
- `components/v2/V2ComponentRenderer.tsx` — `isV2Media` / `V2Media` import + `if (isV2Media(props)) return <V2Media>` 분기 제거
- `components/v2/V2ComponentsDemo.tsx` — `V2Media` import + media TabsTrigger + media TabsContent 통째 제거
- `components/v2/index.ts` — `V2Media` export + V2Media 타입 re-export (`V2MediaType / V2MediaConfig / V2MediaProps`) 제거
- `components/v2/config-panels/index.ts` — `V2MediaConfigPanel` export 제거
- `types/v2-components.ts` — `V2MediaType / V2MediaConfig / V2MediaProps` 인터페이스 + `isV2Media` 가드 + `V2ComponentType` / `V2ComponentProps` 유니온의 V2Media 멤버 + `LEGACY_TO_V2_MAP` 의 file-upload/image-widget → V2Media 매핑 제거
- `components/v2/V2Media.tsx` — D.6 삭제 전까지 전체 tsc 가 중앙 타입 제거로 바로 깨지지 않도록 로컬 orphan `V2MediaProps` 만 임시 정의. 런타임/register/config 경로는 이미 차단됨.
- `app/(main)/screens/[screenId]/page.tsx` — `compType?.includes("v2-media")` / `compType?.includes("file-upload")` 중복 매칭 제거 (이미 `includes("input")` 으로 흡수됨)

**보존한 shared file viewer 유틸**:

| 파일 | 보존 이유 |
|---|---|
| `frontend/lib/registry/components/file-upload/FileViewerModal.tsx` | `GlobalFileViewer.tsx`, `FileAttachmentDetailModal.tsx` 가 import. 단독 파일 뷰어 모달. canonical input 과 무관한 shared util |
| `frontend/lib/registry/components/file-upload/FileManagerModal.tsx` | `FileUploadComponent` (옛 본체) 가 사용. FileViewerModal 사용. 본체 삭제 시 함께 검토 |
| `frontend/lib/registry/components/file-upload/types.ts` (있다면) | FileViewerModal/FileManagerModal 의 타입 의존성 |

**검증**:
- ✅ `git diff --check` — 0건
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer"` — 0건
- ✅ `rg "componentType: \"v2-file-upload\"|file: \"v2-file-upload\"|image: \"v2-file-upload\"|img: \"v2-file-upload\""` — 0건
- ✅ `rg "EntityPicker|entity-picker|EntitySearchModal" canonical input 경로` — 0건
- ✅ `rg "FileUploadComponent" frontend/lib frontend/components frontend/app frontend/types`:
  - 잔존 매치는 모두 (a) 폴더 내부 자기 자신 정의 (`file-upload/`, `v2-file-upload/`) (b) 폴더 내부 V2Media 본체의 import (V2Media 도 폐기 표시됨) (c) Phase D.4/D.5 주석. **외부 runtime 직접 import 0건**
- ✅ `rg "V2Media|v2-media|V2FileUpload|v2-file-upload"`:
  - 잔존은 폐기 폴더 내부 정의 / 옛 본체 (V2Media.tsx / V2FileUploadConfigPanel.tsx) / 폐기 주석. **외부 신규 runtime/config/register 참조 0건**
- ✅ `tsc` 변경 파일 신규 오류 0건 (전역 잔존 오류는 모두 기존 camelCase ↔ snake_case 미스매치)

**다음 phase 에서 실제 삭제 가능한 파일/폴더 (사전 import 0건 재확인 필요)**:

| 경로 | 사전 확인 명령 | 비고 |
|---|---|---|
| `frontend/lib/registry/components/v2-file-upload/` (FileViewerModal/FileManagerModal 제외 가능성 검토) | `rg "v2-file-upload\\|V2FileUpload"` | 폴더 자체 정의만 잔존 |
| `frontend/lib/registry/components/v2-media/` | `rg "v2-media\\|V2MediaRenderer"` | 폴더 자체 정의만 잔존 |
| `frontend/components/v2/V2Media.tsx` | `rg "V2Media\\b"` | 외부 import 0건 확인됨 |
| `frontend/components/v2/config-panels/V2MediaConfigPanel.tsx` | `rg "V2MediaConfigPanel"` | 외부 import 0건 확인됨 |
| `frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx` | `rg "V2FileUploadConfigPanel"` | 외부 사용 없음 |
| `frontend/lib/registry/components/image-widget/` | `rg "image-widget\\|ImageWidget"` | DynamicWebTypeRenderer 의 옛 ImageWidget import 는 Phase D.4 에서 제거됨 |
| `frontend/lib/registry/components/image-display/` | `rg "image-display"` | config panel mapping 제거됨 |
| `frontend/lib/registry/components/file-upload/FileUploadComponent.tsx`, `FileUploadRenderer.tsx`, `FileUploadConfigPanel.tsx` | 위 grep | **FileViewerModal / FileManagerModal / types 만 잔존시키고 본체 3 파일만 삭제** |
| `frontend/components/screen/widgets/types/ImageWidget.tsx` | `rg "ImageWidget"` | DynamicWebTypeRenderer 분기 제거됨 |

**Phase D.6 (제안)**: 위 후보를 각각 grep 0건 확인 후 `git rm`. `FileUploadComponent` 본체만 삭제하고 `FileViewerModal / FileManagerModal / types` 는 보존 (GlobalFileViewer / FileAttachmentDetailModal 의존성).

### Phase D.6 — old file/media 본체 실제 삭제 (2026-05-12)

**배경**: Phase D.4 / D.5 에서 file/image/img 진입 경로는 canonical input + FilePicker 로 전부 통합되고,
옛 file/media 본체의 runtime / register / config 경로도 차단됨. 이 phase 는 사용처 0건이 된 옛 본체를
실제 `git rm` 으로 삭제. shared file viewer 유틸은 `GlobalFileViewer` / `FileAttachmentDetailModal` /
`FileComponentConfigPanel` 의존성이 있어 보존.

**삭제 (git rm)**:
- 폴더 통째: `lib/registry/components/v2-file-upload/` (8 파일) · `v2-media/` (2 파일) · `image-widget/` (3 파일) · `image-display/` (6 파일)
- 본체 파일: `components/v2/V2Media.tsx` · `components/v2/config-panels/V2MediaConfigPanel.tsx` · `components/v2/config-panels/V2FileUploadConfigPanel.tsx` · `components/screen/widgets/types/ImageWidget.tsx`
- file-upload 본체 3 파일: `FileUploadComponent.tsx` · `FileUploadRenderer.tsx` · `FileUploadConfigPanel.tsx` + `config.ts` + `README.md`

**보존 (shared file viewer 유틸)**:
- `lib/registry/components/file-upload/FileViewerModal.tsx` — `GlobalFileViewer` / `FileAttachmentDetailModal` 의존
- `lib/registry/components/file-upload/FileManagerModal.tsx` — FileViewerModal 사용
- `lib/registry/components/file-upload/types.ts` — `FileInfo` / `FileUploadConfig` / `FileUploadResponse` 가 4 곳에서 사용

**정리 (잔여 참조)**:
- `file-upload/index.ts` 재작성 — FileUploadComponent / FileUploadRenderer / FileUploadConfigPanel re-export 제거.
  `FileViewerModal` / `FileManagerModal` + 타입 3종 (`FileInfo` / `FileUploadConfig` / `FileUploadResponse`) 만 export 하는 shim
- `components/screen/widgets/types/index.ts` — `ImageWidget` import / export / `getWidgetComponentByName` 의 `case "ImageWidget"` /
  `getWidgetComponentByWebType` 의 image/img/picture/photo 분기 / `WebTypeComponents` 의 `image` 매핑 모두 제거

**잔존 grep 매치 분류 (모두 운영 영향 없음)**:
- 폐기 안내 주석 / 작업 노트
- `WidgetRenderer.tsx:41` 의 `isImageWidget` 변수 — widgetType 문자열 (`image/img/picture/photo`) 매칭만. ImageWidget 본체 import 없이 wrapper `pointer-events-none` 처리, 실제 렌더는 `DynamicWebTypeRenderer` → canonical input (Phase D.4)
- `pop-components/pop-text.tsx` 의 `ImageDisplay` — POP 컴포넌트 영역의 내부 helper 함수. 폐기된 `image-display/` 폴더와 무관

**검증**:
- ✅ `git diff --check` — 0건
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer"` — 0건
- ✅ `rg "componentType: \"v2-file-upload\"|file: \"v2-file-upload\"|image: \"v2-file-upload\"|img: \"v2-file-upload\""` — 0건
- ✅ `rg "EntityPicker|entity-picker|EntitySearchModal" canonical input 경로` — 0건
- ✅ `rg "FileUploadComponent|FileUploadRenderer|FileUploadConfigPanel|V2Media\b|V2MediaConfigPanel|V2FileUpload|ImageWidget|ImageDisplay"`:
  - 잔존은 모두 폐기 주석 / `isImageWidget` 변수 / `pop-text` 의 ImageDisplay 내부 helper. **외부 runtime / config / register 직접 import 0건**
- ✅ `tsc --noEmit` — 삭제로 인한 `Cannot find module` 오류 0건. 잔존 3건은 모두 D.6 과 무관한 기존 module 오류 (`hierarchyColumn` / `auto-animate` / `lib/types/screen`)

**파일 통계**:
- 삭제: 13 단일 파일 + 4 폴더 (총 ~30 파일)
- 수정: 2 파일 (`file-upload/index.ts` shim 재작성, `widgets/types/index.ts` ImageWidget 정리)

### Phase A.8 — 채번 admin 페이지 + 시퀀스 관리 (★ 별도 트랙)

**배경**: VEX 는 채번을 캔버스 컴포넌트로 처리 (잘못된 구조). 팀장 요구 — 별도 admin 페이지에서 채번 규칙 + 시퀀스 일원 관리. INVYONE 에는 그 기능 없음 → 신규 작성.

**작업 중 발견한 mismatch 들 (운영 전이라 모두 동시 fix)**:

1. **URL mismatch** — backend Controller `/api/numbering-rule` (단수) ↔ frontend `/numbering-rules` (복수)
   - **fix**: backend `@RequestMapping` 복수로 변경
2. **응답 key mismatch** — backend `Map.of("code", ...)` ↔ frontend `data.generatedCode`
   - **fix**: backend 4 endpoint (preview/test-preview/allocate/generate) 응답 key 모두 `generatedCode` 로 통일
3. **GREATEST 로직** — `updateCurrentSequenceInRule` mapper 가 GREATEST 로 sequence 못 내림
   - **fix**: admin 전용 `setCurrentSequenceInRule` SQL 신규 (GREATEST 없이 직접 SET). 기존 mapper 는 allocateCode 흐름용으로 유지
4. **두 테이블 ground truth 분리** — 실제 발번은 `numbering_rule_sequences` (prefix 별), `numbering_rules.current_sequence` 는 표시용
   - **fix**: admin 의 reset/update 가 두 테이블 다 처리:
     - `deleteSequencesByRuleId` (prefix sequences 비움)
     - `setCurrentSequenceInRule` (numbering_rules 직접 set)
   - 추가: `incrementSequenceForPrefix` 의 INSERT 분기에서 base 값을 `numbering_rules.current_sequence + 1` 로 변경 → admin set 값 N 이 다음 발번 N+1 로 정확히 반영
5. **권한 없음** — PUT /sequence + POST /reset 일반 사용자도 호출 가능
   - **fix**: `@RequestAttribute("role")` 받아서 SUPER_ADMIN/ADMIN/COMPANY_ADMIN 만 허용. 403 으로 거부
6. **Designer wrong rule 위험** — `NumberingRuleDesigner` lockedColumn 시 by-column 조회로 카테고리 분기 규칙에서 다른 rule 열림
   - **fix**: 기존 `initialConfig` prop 활용. mount 시 setCurrentRule + setSelectedColumn 직접 set. lockedColumn effect 에 `if (initialConfig) return` 가드
7. **race condition** — SequenceManagementPanel 의 저장/리셋 동시 실행
   - **fix**: 단일 `mutating: null | "save" | "reset"` state 로 통합. 양 버튼 disabled 가 `mutating !== null`

**신규 / 수정 파일**:

- ✅ `backend-spring/src/main/java/com/erp/controller/NumberingRuleController.java`
  - `@RequestMapping` 단수 → 복수
  - `PUT /{ruleId}/sequence` endpoint 신규 (권한 체크 포함)
  - `POST /{ruleId}/reset` 에 권한 체크 추가
  - preview/allocate/generate 응답 key 통일
  - `isAdminRole` 헬퍼 추가
- ✅ `backend-spring/src/main/java/com/erp/service/NumberingRuleService.java`
  - `updateRuleSequence` 메서드 신규 (두 테이블 처리)
  - `resetSequence` 도 `setCurrentSequenceInRule` 사용으로 변경
  - `incrementSequenceForPrefix` 의 INSERT 분기 base = numbering_rules.current_sequence + 1
- ✅ `backend-spring/src/main/resources/mapper/numberingRule.xml`
  - `setCurrentSequenceInRule` SQL 신규 (admin 전용)
- ✅ `frontend/lib/api/numberingRule.ts`
  - `updateRuleSequence(ruleId, newSequence)` 함수 신규
- ✅ `frontend/components/numbering-rule/SequenceManagementPanel.tsx` 신규
  - 현재 시퀀스 + 직접 수정 + reset + 미리보기. mutation lock 적용
- ✅ `frontend/components/numbering-rule/NumberingRuleDesigner.tsx`
  - `initialConfig` prop 활용 effect 추가 (by-column 우회)
  - `selectedColumn` 초기 state 를 lockedColumn 으로
- ✅ `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx` 신규
  - 좌측 규칙 목록 + 우측 ① NumberingRuleDesigner + ② SequenceManagementPanel
  - 신규 규칙 CTA → NumberingRuleCreateDialog
- ✅ `frontend/components/layout/AdminPageRenderer.tsx`
  - `/admin/systemMng/numberingRuleList` 라우팅 등록

**남은 / 후속**:

- ✅ 사이드바 메뉴 DB INSERT (2026-05-11 완료)
  - Flyway: `V017__register_numbering_rule_menu.sql` + `V018__assign_numbering_rule_menu_to_super_admin.sql`
  - 즉시 운영 DB INSERT 실행: MENU_INFO row 등록 (OBJID=NUMBERING_RULE_LIST, SEQ=8, PARENT=시스템 관리 그룹)
  - AUTHORITY_SUB_MENU 매핑: 운영 DB 에 SUPER_ADMIN AUTH_CODE 가 없어 INSERT 0 (다른 systemMng 메뉴도 매핑 없이 동작 중. 무해)
- ✅ v2-numbering-rule / numbering-rule 캔버스 컴포넌트 폐기 (2026-05-11 완료)
  - `lib/registry/components/numbering-rule/` 폴더 통째 삭제
  - `lib/registry/components/v2-numbering-rule/` 폴더 통째 삭제
  - `components/v2/config-panels/V2NumberingRuleConfigPanel.tsx` 삭제
  - `components/screen/templates/NumberingRuleTemplate.ts` 삭제 (dead code)
  - `lib/registry/components/index.ts` 의 두 import 제거
  - `lib/utils/getComponentConfigPanel.tsx` 의 두 import 제거
  - `ComponentsPanel.tsx` 의 hidden list 에서 "v2-numbering-rule" / "numbering-rule" 제거
- ⏳ 운영 모드 동작 검증 (backend 재시작 필요 — URL mismatch + 응답 key + sequence 흐름 다 fix 됐는지 확인)

### Phase A.7 — 스튜디오 내 채번 규칙 생성 (CTA + Dialog)

사용자 요구: 운영에서 채번 규칙은 admin 페이지에서 사전 등록되어야 NumberingPicker 동작. 스튜디오에서도 새 규칙을 만들 수 있게 — InvFieldConfigPanel 의 채번 옵션에 "새 규칙 만들기" CTA + Modal.

- ✅ `NumberingRuleDesigner.tsx` — `lockedColumn?: { tableName, columnName }` prop 추가
  - 좌측 컬럼 목록 UI hide (조건부 render)
  - mount 시 자동 `handleSelectColumn` 호출
  - selectedColumn 초기 state 를 lockedColumn 으로 (flash 방지)
  - "컬럼을 선택하세요" → lockedColumn 시 "채번 규칙을 불러오는 중..." 로 분기
- ✅ `frontend/components/numbering-rule/NumberingRuleCreateDialog.tsx` 신규 — shadcn Dialog wrapper. NumberingRuleDesigner 를 lockedColumn 으로 띄움. onSave → onCreated callback + 자동 닫기
- ✅ `InvFieldConfigPanel.tsx` — NumberingOptions 에 "+ 새 규칙 만들기" 버튼 + Dialog 통합
  - `rulesRefreshKey` state + numberingRules effect dep 추가
  - 생성 후 새 ruleId 자동 선택 + rules 목록 refresh
  - **canonical 위치 통일**: `autoGeneration.numberingRuleId` (옵션 밖) → `autoGeneration.options.numberingRuleId` (옵션 안)
    - 옛 입력 설정 패널도 옵션 안에 저장. autoGeneration.ts.generateValue 도 옵션 안 사용.
    - InputComponent 의 NumberingPicker `numberingRuleId` prop 도 옵션 안에서 읽음 → 일관 ★
    - 이전 옵션 밖 fallback 제거 (canonical 1안 원칙)
- ✅ Codex 검증 — 2 issue fix
  - **MED**: applyRuleId 에 `enabled: true` + `options.numberingRuleId` 명시 (저장 시 무효 방지)
  - **LOW**: selectedColumn 초기 state 를 lockedColumn 으로 (flash 방지)
  - **OK** 항목 (3·5·6) 무수정
  - **LOW** (4 토큰 컨벤션 raw hsl) — 기능 영향 없음, 후속 정리

### Phase A.6 — numbering API hook (완료)

- ✅ `frontend/lib/registry/components/input/numbering-picker.tsx` 신규
  - 옛 입력 컴포넌트의 채번 본체 추출 — useState/useRef + 3 useEffect (main / debounce / beforeFormSave) + 렌더 (readonly text vs prefix-input-suffix)
  - `previewNumberingCode(ruleId, formData, manualInputValue)` API 사용
  - ruleId 결정 흐름: `props.numberingRuleId` 우선 → 없으면 `by-column` API → fallback `getTableColumns.detailSettings.numberingRuleId`
  - `____` 템플릿: 첫 생성 시 templateRef set + 부모 value, 카테고리 변경 시 manualInputValue 유지 + 새 조합값 onChange
  - debounce (300ms) 디바운스로 manualInputValue 변경 시 suffix 동적 갱신
  - beforeFormSave window listener — 저장 직전 조합값 formData 주입
- ✅ Codex 검증 (1차) — 6 issue 지적 → 전부 fix 적용
  - cancellation flag (main + debounce) → race condition 방지
  - `??` → `||` (tableName 폴백, 빈 문자열 통과 방지)
  - `originalData || _originalData` 별칭 보강 (isEditMode)
  - 카테고리 변경 + userEditedRef 시 onChange 호출 (parent value 누락 fix)
  - propRuleId main effect dep 추가 + isDesignMode debounce dep 추가
  - inputSlotStyle 에 `overflow: hidden + borderRadius: inherit` 이동 (NumberingPicker 모서리 처리)
  - `_numberingRuleId` callback (EditModal/buttonActions 호환) — `onRuleIdResolved` prop 추가
  - Legacy `inputType="numbering"` → "code" 매핑 추가 (V2 저장 데이터 호환)
- ✅ InputComponent.tsx — `case "code"` 분기 → NumberingPicker 호출. tableName 5경로 (props/config/component/overrides/screenInfo) + isEditMode 노출
- ✅ 타입체크 통과 (numbering-picker / InputComponent 에서 에러 0건)

### Phase B.4.4 — TogglePicker

- ✅ `TogglePicker` 신규 — boolean Y/N 토글 스위치 (truthy 자동 판정: bool/숫자/Y/N/true/false/1/0)
- ✅ InputComponent `case "checkbox"` 분기 — `mode=toggle` 시 TogglePicker, 아니면 기존 단일 체크박스

### Phase B.3 — TagPicker

- ✅ `TagPicker` 신규 — chip + 입력 (Enter / 구분자 추가, Backspace / ✕ 제거, maxSelect 차단, 중복 방지)
- ✅ InputComponent multi 분기 — `format=tags` 또는 `mode=tag` 시 TagPicker

### Phase B.4 추가 정리 (사용자 사진 검증 반영)

- ✅ Radio/Checkbox list 의 **외각 box 제거** — `mode=radio||check` 시 container border/bg 없음 (자체 visual element 가 표시)
- ✅ **"복수 선택" CPSwitch 제거** (고급 설정) — brumb 의 `단일/다중` 이 진실의 원천. 단일/다중 = 저장 형태 차이 (값 cardinality), UX 토글 아님
- ✅ "최대 개수" 노출 조건 — `multi prop` 으로 분기 (config.multiple 의존 X)
- ✅ ConfigPanel "선택 방식" 옵션을 multi 따라 분기 — single: dropdown/combobox/radio/toggle, multi: dropdown/combobox/check/swap/tag
- ✅ ~~`displayMode` 신규 prop~~ 폐기 — 기존 `config.mode` 와 중복이라 정리. mode 만 사용
- ✅ Radio/Checkbox list — flex-col → flex-wrap 으로 변경 (디자인/운영 모드 외형 일관, 박스 사이즈에 맞게 자동 wrap)
- ✅ picker 4개 wrapper 의 `opacity-50` 제거 (시각 흐릿 해소)
- ✅ InputComponent select 분기 disabled 에 `isDesignMode` 빼기 — 디자인 모드에서도 클릭 가능

### Phase B.5 — CPSelect Portal 도입

- ✅ `CPSelect` 의 dropdown 을 React Portal 로 — `position: fixed` + `getBoundingClientRect()` 좌표 + scroll/resize 시 닫음
- ✅ ConfigPanel 의 `overflow:auto` 와 무관하게 dropdown 화면 끝까지 보임 (사진 #35 의 가려짐 해소)

### Phase B.6 — defaultValue 버그 (중요 발견)

**증상**: 사용자가 ConfigPanel 에 "기본 선택값: 저는 김민호" 설정해도 운영 (form-popup 수정 모달) 에서 default 안 적용. InputComponent props 의 `componentConfig.defaultValue: undefined`.

**진단 단계** (긴 디버그 흐름):
1. `[Input debug]` console.log 추가 → 스튜디오 OK, 운영 undefined 확인
2. `[saveTemplate]` 디버그 → edit 뷰 `overrides.defaultValue = "저는 김민호"` 저장 정상
3. `[loadTemplate]` 디버그 → editLegacy `componentConfig.defaultValue = "저는 김민호"` load 정상
4. `[Input render path]` 디버그 → `pageUrl: "/form-popup"` 확인, rawComponentConfig.defaultValue = undefined
5. → form-popup 페이지가 **BlockRenderer** 사용. 거기서 stripping

**진짜 원인**: `frontend/components/dash/BlockRenderer.tsx:54-57`

```ts
// BEFORE (버그)
const runtimeConfig =
  resolvedColumnName != null
    ? { ...block.config, defaultValue: resolvedValue }  // ← 무조건 덮어씀
    : block.config;
```

→ columnName 있는 컴포넌트는 form data 의 그 컬럼 값 (`context.formRow?.[columnName]`) 으로 **defaultValue 를 무조건 hijack**. 신규/빈 row 시 `resolvedValue = undefined` → 사용자 설정 defaultValue 가 undefined 로 덮임.

**Fix**:
```ts
// AFTER
const runtimeConfig =
  resolvedColumnName != null && resolvedValue !== undefined && resolvedValue !== null
    ? { ...block.config, defaultValue: resolvedValue }
    : block.config;  // ← formRow 값 없으면 사용자 설정 defaultValue 보존
```

### 잘못 들어간 경로 (rollback 완료)

- ❌ ~~GPT 진단 — frontend `saveLayoutV2` 의 payload shape 와 backend `body.get("layout_data")` 불일치~~
  - 처음 GPT 가 `saveLayoutV2` payload 를 `layout_data` 키로 감싸야 한다 제안
  - 사용자 짚음: "그러면 옛 컴포넌트는 어떻게 저장됐냐" — 정확. 모순.
  - **저장 자체는 정상**이었음. INVYONE 스튜디오는 `saveTemplate` 사용 (template_id 가 있을 때), saveLayoutV2 는 별개 경로
  - rollback 완료. 정상 흐름 복원

### 임시 디버그 (잔존 — 정리 가능)

- `templateAdapter.ts:76` 의 `[saveTemplate] payload` 일반 진단용 로그 — 유지 (low-noise)
- 그 외 [Input debug] / [Save layout debug] / [loadTemplate] / [Input render path] 등 모두 제거 완료

### Phase B.5 — option loader (2026-05-12)

**배경**: InputComponent 의 select 분기가 `componentConfig.options` 정적 배열만 처리했음.
공통코드 (`source=code`) / 사용자 카테고리 (`source=category`) / DB distinct (`source=distinct`)
/ 외부 API (`source=api`) 케이스는 옛 선택 컴포넌트의 옵션 로딩 로직에 갇혀 있어서 input
canonical 만으로는 옵션이 비어 보였음.

**변경**:
- ✅ `frontend/lib/registry/components/input/use-option-loader.ts` 신규 hook
  - 입력 : `config / tableName / columnName / formData / isDesignMode`
  - 처리 source : `static / code / category / distinct (=select) / api / db`
  - 응답 정규화 : `Array<{value, label}>` — `value/code/id/valueCode` + `label/name/valueLabel`
    여러 응답 shape 흡수. category 트리는 `flattenCategoryTree` 로 평탄화 (`valueCode` 우선)
  - 디자인 모드 가드 : `isDesignMode === true` 면 fetch 자체 skip → static 만
  - 캐시 : module-scoped `Map<url, options>` 로 같은 url 재호출 차단
  - race 방지 : 요청 id (`reqIdRef`) + `cancelled` flag
  - 계층 (`hierarchical + parentField`) : `parentValue` 가 `formData[parentField]` 에서
    오면 `/common-codes/categories/{group}/hierarchy?parentCodeValue=...` 로 자식 코드 조회
  - entity 는 참조 테이블의 value/label 컬럼을 code-name 옵션으로 로드
  - 실패 시 `console.warn` + 빈 옵션 (컴포넌트가 죽지 않음)
- ✅ `InputComponent.tsx` 본체 — `useOptionLoader` 호출 + select 분기에서 `loadedOptions`
  를 picker 들 (SingleSelectPicker / MultiSelectPicker / RadioPicker / CheckboxListPicker)
  에 직접 전달. 정적 정규화 코드 (rawOptions.map) 제거
- ✅ FieldOption (string | object) 과 SelectOption (object) 의 union vs concrete 충돌
  해소를 위해 loader 가 자체 `LoadedOption` 타입 정의해 반환

**구현한 source**: static / code / category / distinct / select / api / db (V2 legacy)

**TODO 로 남은 source / 기능**:
- 다단 cascade (계층 외에 sourceFiltering 으로 N→1 의존)
- 옵션 검색 `filters` (`OptionFilter[]`) — runtime 치환 후 query string 전달.
  InvFieldConfigPanel 에 filter UI 가 아직 없어서 데이터 자체가 없음. config 가 들어오면
  쉽게 확장 가능하도록 hook 내부에 자리만 잡아둠 (현재는 미적용)
- ✅ ~~swap 모드~~ (B.4.5 — 2026-05-12 완료)

**검증**:
- ✅ `git diff --check` 통과 (whitespace 0)
- ✅ `rg V2SelectRenderer | v2-input/V2InputRenderer | id: "v2-input" | id: "v2-select" |
  componentType: "v2-input/select" | type: "v2-input/select"` — 외부 import 0건
  (`v2-input` / `v2-select` 렌더러 폴더 자체 정의는 Phase D에서 삭제 완료)
- ✅ `npx tsc --noEmit --pretty false` 변경 파일 (InputComponent / use-option-loader)
  타입 오류 0건. `DateInputComponent.tsx:214` 의 기존 옛 에러는 Phase E 폐기 예정

### Phase D.1 — v2-input/v2-select 렌더러 파일 삭제 (2026-05-12)

**배경**: Phase C.2/B.5 이후 `v2-input` / `v2-select` 는 더 이상 registry/import 경로에서 쓰이지 않음. 새 솔루션 개발 기준이므로 기존 저장 화면 호환 / DB layout 마이그레이션은 목표가 아님.

**변경**:
- ✅ `frontend/lib/registry/components/v2-input/V2InputRenderer.tsx` 삭제
- ✅ `frontend/lib/registry/components/v2-input/index.ts` 삭제
- ✅ `frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx` 삭제
- ✅ `frontend/lib/registry/components/v2-select/index.ts` 삭제
- ✅ 혼선 방지용 주석 정리 — 새 생성 경로는 `input` canonical 이며, legacy alias/fallback 은 Phase D.2 에서 제거 대상

**검증**:
- ✅ `rg components/v2-input|components/v2-select|V2InputRenderer|V2SelectRenderer` — 실제 참조 0건
- ✅ `rg id/componentType/type: "v2-input|v2-select"` — 신규 생성 literal 0건
- ✅ `git diff --check` 통과

### Phase B.4.5 — SwapPicker (2026-05-12)

**배경**: InputComponent 의 select multi 분기가 `dropdown/multi/radio/check/tag` 까지 처리하지만
`mode=swap` 은 미구현. 양쪽 리스트로 항목을 이동하는 UI 가 옛 다중 선택의 표준 패턴 중 하나라
canonical input 에 흡수.

**변경**:
- ✅ `frontend/lib/registry/components/input/select-pickers.tsx` — `SwapPicker` 신규 export
  - props: `value: string[] / onChange / options / maxSelect / disabled / readonly / className / availableLabel / selectedLabel`
  - UI: 왼쪽 (available, options 순서) ↔ 가운데 (`ChevronRight`/`ChevronLeft` 버튼) ↔ 오른쪽 (selected, value 순서)
  - 항목 클릭 → 같은 패널 내 highlight 토글 (다중 선택). 이동 후 highlight 자동 해제
  - 순서 정책: left→right 이동 시 selected 끝에 append, right→left 이동 시 그 항목만 제거
  - options 에 없는 value 가 들어와도 selected 에 표시 (label = value fallback)
  - opt.disabled / 패널 lockEdit 항목은 클릭 차단 + opacity 60%
- ✅ `frontend/lib/registry/components/input/InputComponent.tsx` — select multi 분기 `mode === "swap"` 추가
  - 위치: tag → check → **swap** → dropdown/combobox (기본) 순. 기존 분기 보존
  - `loadedOptions` 그대로 전달, `maxSelect` / disabled / readonly 동일하게 전달
- ✅ `frontend/components/v2/config-panels/InvFieldConfigPanel.tsx` — 고급 설정의 `선택 방식` 에
  `swap` 노출. `multi + list` 는 패널 설명처럼 기본 `mode=check` 로 정렬

**maxSelect / readonly / disabled 처리**:
- `maxSelect`: left→right 이동 시 `selected.length + additions.length > maxSelect` 면 `slice(0, maxSelect)` 로 잘림.
  selected 가 이미 한도면 right 화살표 버튼 자체 비활성 (`atLimit`).
- `readonly` / `disabled`: 단일 `lockEdit` 플래그로 통합. 항목 클릭, 화살표 버튼, 패널 hover 효과 모두 비활성.
  컨테이너에 `cursor-not-allowed` 부착.

**검증**:
- ✅ `git diff --check` 통과 (whitespace 0)
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer"` — 0건 유지
- ✅ `rg "V2Input|V2Select|V2InputConfigPanel|V2SelectConfigPanel"` — `V2SelectedItemsDetailInputConfigPanel`
  substring 매치만 잔존 (unrelated 컴포넌트)
- ✅ `npx tsc --noEmit` — InputComponent / select-pickers / use-option-loader 신규 오류 0건

### Phase D.2 — v2-input/v2-select alias/fallback/schema 완전 제거 (2026-05-12)

**배경**: 목표는 InvFieldConfigPanel UI 호환이 아니라 FieldConfig / DataPort 계약 호환. V2 입력/선택 구현체를 runtime alias/fallback/schema 로 살리지 않고, 필요한 기능만 canonical `input` 에 흡수한다.

**변경**:
- ✅ `DynamicComponentRenderer.tsx` — legacy alias / V2SelectRenderer 직접 require / 입력·선택 특수 처리 제거
- ✅ `getComponentConfigPanel.tsx`, `V2PropertiesPanel.tsx` — V2 입력/선택 패널 매핑 제거
- ✅ `componentConfig.ts` — V2 입력/선택 override schema / default config 제거
- ✅ 화면/디자이너/aggregation 패널/CSS/templateMigrate 주석·분기 제거
- ✅ `InputComponent.tsx` — 옛 `rawType` fallback 분기 제거

**검증**:
- ✅ `git diff --check` 통과
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer" frontend/lib frontend/components frontend/app frontend/types frontend/styles` — 0건
- ✅ `rg "components/v2-input|components/v2-select|v2-input/V2InputRenderer|v2-select/V2SelectRenderer" frontend` — 0건
- ✅ FieldConfig / DataPort 계약 축소 없음. `types/invyone-component.ts` 변경은 entity FieldRef 확장(autoFill/filter/modalColumns)만.

---

## 3. 진행 중 / 남은 작업

### Phase A 잔여

- ✅ ~~**A.6** numbering API hook~~ (2026-05-11 완료 — NumberingPicker 신규 + InputComponent 통합)

### Phase B 진행

- ✅ ~~**B.2** MultiSelectPicker — multi / maxSelect~~ (완료)
- ✅ ~~**B.3** TagPicker — tags (tagbox)~~ (완료)
- ✅ ~~**B.4** radio / checkbox / toggle~~ (완료)
- ✅ ~~**B.4.5** SwapPicker — multi + mode=swap~~ (2026-05-12 완료 — select-pickers.tsx 에 신규)
- ✅ ~~**B.5** option loader — api / code / category / distinct / 계층~~ (2026-05-12 완료 — `use-option-loader.ts` 신규)
- ✅ webTypeMapping 의 checkbox / radio / boolean / code / category / entity 매핑 → input
- ⏳ 실제 옵션 주입 검증 — 공통코드 / 카테고리 데이터 있는 화면에서 운영 동작 확인 필요

### 디버그 (해결)

- ✅ ~~`config.defaultValue` 동작 안 함~~ — BlockRenderer hijack 버그 (2026-05-11 해결)

### Phase C — entity

- ✅ entity code-name 옵션 로딩 (`/entity/{table}/options?value=...&label=...`)
- ✅ InputComponent entity 분기 → Single/Multi select picker
- ✅ multi entity (`type="multi" + format="entity"`) 는 JSON 배열 저장 형태로 select 계열 picker 재사용

### Phase D — V2 폐기

- ✅ webTypeMapping 의 select 계열 (category/entity/code/checkbox/radio/boolean) → input
- ⏳ webTypeMapping 의 file/image/img → input (file 통합 후)
- ✅ `ScreenSettingModal.tsx:2052-2066` 의 옛 컴포넌트 생성 경로 → input
- ✅ DB layout JSON 마이그레이션 안 함 — 새 솔루션 개발 기준, 기존 V2 저장 데이터 보존은 목표 아님
- ✅ `frontend/lib/registry/components/v2-input/` 및 `v2-select/` 렌더러 파일 삭제
- ✅ `registerV2Components.ts` 의 v2-input / v2-select 등록 제거
- ✅ `components/index.ts` 의 v2-input / v2-select renderer auto-register 제거
- ✅ v2-input / v2-select alias / fallback / schema 제거
- ✅ `V2Input.tsx` / `V2Select.tsx` 본체 및 `V2InputConfigPanel.tsx` / `V2SelectConfigPanel.tsx` 삭제

### Phase D.3 — V2Input / V2Select 본체 및 직접 참조 삭제

- ✅ `frontend/components/v2/V2Input.tsx` 삭제
- ✅ `frontend/components/v2/V2Select.tsx` 삭제
- ✅ `frontend/components/v2/config-panels/V2InputConfigPanel.tsx` 삭제
- ✅ `frontend/components/v2/config-panels/V2SelectConfigPanel.tsx` 삭제
- ✅ `components/v2/index.ts`, `config-panels/index.ts`, `V2ComponentRenderer.tsx`, `V2ComponentsDemo.tsx`, `DynamicConfigPanel.tsx` 직접 참조 제거
- ✅ `types/v2-components.ts` 의 옛 입력/선택 타입·type guard·legacy map 제거
- ✅ `V2SelectFilter` 의존을 canonical `OptionFilter` 로 이전
- ✅ `rg "V2Input|V2Select|V2InputConfigPanel|V2SelectConfigPanel|components/v2/V2Input|components/v2/V2Select" frontend/lib frontend/components frontend/app frontend/types`
  결과는 `V2SelectedItemsDetailInputConfigPanel` substring 매치만 잔존 (별개 컴포넌트)
- ✅ `rg "v2-input|v2-select|V2InputRenderer|V2SelectRenderer" frontend/lib frontend/components frontend/app frontend/types frontend/styles` — 0건
- ✅ FieldConfig / DataPort 계약 축소 없음. 이번 변경으로 `invyone-component.ts` / `dataPort/runtime.ts` 추가 변경 없음.

### Phase E — 옛 6개 폐기

- ⏳ date-input / text-input / number-input / select-basic / checkbox-basic / textarea-basic 의 캔버스 컴포넌트 (`DateInputComponent.tsx` 등) 폐기
- ⏳ 6 폴더 자체 삭제 (ScreenSettingModal 의 생성 경로 검증 후)
- ⏳ 부수 효과 — `DateInputComponent.tsx:214` 의 옛 TS 에러 자연 해소

---

## 4. 위험 영역 / 주의사항

1. **옛 입력/선택 고유 기능 이식 검증**
   - 이식 완료: numbering API, password/email/tel/url, money/number, radio/check/toggle/swap, entity code-name option, option loader(api/code/category/distinct/계층)
   - 남은 항목: mask, slider, color picker, file/image/img 통합

2. **DB layout 마이그 금지**
   - 새 솔루션 개발 기준이므로 기존 저장 화면 보존은 목표가 아님.
   - `screens` / `templates` JSON 안 옛 componentType 을 변환하는 SQL 작성 금지.
   - 정리 대상은 코드의 생성/렌더/설정/schema 경로이며, FieldConfig / DataPort 계약 호환을 우선한다.

3. **dev reload 캐시**
   - webTypeMapping.ts 변경은 새로 끌어 놓는 컴포넌트만 반영
   - 캐시 안 잡히면 hard reload (Cmd+Shift+R)

4. **canonical pattern — TYPE_VOLATILE_FIELDS**
   - 새 type 추가 시 잔재 필드도 같이 등록할 것 (안 그러면 분기 간 잔재 → 옛 컴포넌트 분기 트리거 위험)

---

## 5. 관련 파일 맵

```
[Canonical (목표)]
frontend/components/v2/config-panels/InvFieldConfigPanel.tsx  ← 단일 ConfigPanel (brumb)
frontend/lib/registry/components/input/InputComponent.tsx     ← 단일 캔버스 컴포넌트
frontend/lib/registry/components/input/pickers.tsx            ← date/datetime/time/daterange picker
frontend/lib/registry/components/input/select-pickers.tsx     ← select picker (B.1 신규)
frontend/lib/registry/components/input/index.ts               ← input 등록 (config_panel: InvFieldConfigPanel)

[라우팅]
frontend/lib/utils/webTypeMapping.ts                          ← web_type → componentType
frontend/lib/utils/getComponentConfigPanel.tsx                ← componentId → ConfigPanel
frontend/lib/registry/DynamicComponentRenderer.tsx            ← 캔버스 렌더 dispatch (fieldType swap 제거됨)
frontend/components/screen/panels/V2PropertiesPanel.tsx       ← properties 패널 라우팅

[폐기 완료]
frontend/components/v2/V2Input.tsx                            ← 삭제 완료
frontend/components/v2/V2Select.tsx                           ← 삭제 완료
frontend/components/v2/config-panels/V2InputConfigPanel.tsx   ← 삭제 완료
frontend/components/v2/config-panels/V2SelectConfigPanel.tsx  ← 삭제 완료

[폐기 예정]
frontend/lib/registry/components/{date,text,number}-input/    ← 자체 캔버스 컴포넌트 폐기
frontend/lib/registry/components/{select,checkbox,textarea}-basic/

[유틸 / API]
frontend/lib/utils/autoGeneration.ts                          ← AutoGenerationUtils.generateValue (A.5 hook 연결됨)
frontend/lib/api/numberingRule.ts                             ← previewNumberingCode (A.6 이식 예정)
```

---

## 6. 핵심 의사결정 기록

| 결정 | 이유 |
|---|---|
| Canonical = FieldConfig/DataPort + InputComponent | FieldConfig 가 유일한 필드 규격, DataPort 가 컴포넌트 통신 계약 |
| (가)/(다) 하이브리드 거부 → (나) 통합 추진 | canonical 1안 원칙 / 운영 단계 아님 |
| webTypeMapping → input 점진 변경 | V2Select 고유 기능 이식 후 매핑 변경 (안전) |
| native `<select>` → SingleSelectPicker | OS 기본 dropdown 통일 어려움 + V2Select 풍부 기능 흡수 |
| `TYPE_VOLATILE_FIELDS` reset 패턴 | 명시 cleanup 보다 유지보수성 + 새 type 자동 일관 |
| label position: absolute (박스 바깥) | V2 스타일 / 사용자 의도 |
| 옛 데이터 마이그 X | 운영 단계 아님 (사용자 명시) |

---

## 7. 다음 세션 진입점

### 우선순위

1. **D 후속 — file/image/img → input 통합**
   - webTypeMapping 의 file/image/img 경로
   - 기존 file-upload / image-widget / v2-media 기능 중 필요한 것만 canonical input 으로 흡수

2. **C 후속 — entity filter runtime** (중)
   - `filters` 를 entity/category/code option query 에 runtime 적용
   - field/user/static 값 치환 후 API query 전달

3. **D — 남은 입력계 폐기**
   - mask / slider / color picker 흡수 여부 결정

4. **E — 옛 6개 폐기**
   - date-input / text-input / number-input / select-basic / checkbox-basic / textarea-basic 의 캔버스 컴포넌트 (`DateInputComponent.tsx` 등) 폐기
   - 6 폴더 자체 삭제 (ScreenSettingModal 의 생성 경로 검증 후)

### 새 세션 진입 전 준비

- ✅ defaultValue 동작 검증 완료 — form-popup 수정 모달에서 default 적용 됨
- 이전에 한 변경들은 form-popup 의 BlockRenderer hijack 으로 동작 검증이 어려웠음. 이제 가능
- ⏳ D 후속 / C 후속 / E 중 어디부터 진행할지 결정 — 사용자 의도 (사진 확인 가능한 것 우선) 따라

### 핵심 사실 (새 세션이 알아야 할 것)

1. **INVYONE 스튜디오 = templates 모드** (`template_id` 가 있는 경우 `saveTemplate` 호출, `saveLayoutV2` 아님)
2. **운영 모드 form-popup** (`/form-popup?templateId=...`) 가 `getTemplateInfo` → PopupTemplateRenderer → BlockRenderer → InputComponent 경로
3. **edit 뷰 컴포넌트** 가 운영에서 보임 (수정 모달)
4. **BlockRenderer.tsx:54-57** 의 runtimeConfig 가 columnName 기반 defaultValue hijack — fix 적용됨
5. 임시 console.log 모두 제거됨 (saveTemplate payload 의 일반 진단 로그만 1줄 유지)

---

## Closing (2026-05-13)

입력 canonical 마이그레이션은 본 phase 시리즈로 종결됨.

- **단일 진실의 원천**: `frontend/lib/registry/components/input/InputComponent.tsx` (canonical input)
- **단일 ConfigPanel**: `frontend/components/v2/config-panels/InvFieldConfigPanel.tsx` (id: `"input"`)
- **삭제된 폴더 11개**: 옛 입력 6종 + slider-basic + radio-basic + toggle-switch + test-input + `category-manager.tsx` orphan
- **보존 explicit/domain 7종**: entity-search-input, autocomplete-search-input, selected-items-detail-input, domain/v2-location-swap-selector, v2-category-manager, category-manager/, mail-recipient-selector
- **병합 커밋**: `7bd08dcf9 refactor(components): consolidate canonical input cleanup`

상세 phase 별 작업과 검증 명령은 `notes/gbpark/2026-05-12-codex-handoff-input-canonical.md` §6 참고.
