Files
invyone/notes/gbpark/2026-05-08-input-canonical-migration.md
T

62 KiB
Raw Blame History

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.enabledAutoGenerationUtils.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 / dropdowninput (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 의 기본 동작이 아니다.

변경:

  • useOptionLoadersource=entity/entity/{entityTable}/options?value={entityValueColumn}&label={entityLabelColumn} 로 로드
    • config.ref.table/valueColumn/displayColumn 도 같은 계약으로 처리
  • InputComponenttype=single|multi + format=entity 는 select 계열 picker 로 렌더
    • 단일 entity: SingleSelectPicker (검색 가능 dropdown)
    • 다중 entity: MultiSelectPicker / mode=check / mode=swap 지원
  • 잘못 추가된 canonical input 전용 EntityPicker 모달 경로 제거
  • V2PropertiesPanel / InvFieldConfigPanel — 참조 테이블 목록에서 table_name/display_nametableName/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.tsv2-input/V2InputRenderer, v2-select/V2SelectRenderer auto-register import 제거
  • registerV2Components.tsv2-input, v2-select 레지스트리 등록 제거
  • InputComponent.tsxformat="entity" 는 select 계열 picker 로 라우팅
  • useOptionLoader.tsentityTable/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.tssource=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.normalizeConfigPanelTablestableName/displayNametable_name/display_name 모두 흡수. InvFieldConfigPanel 에 tables / allTables 정규화된 값 전달
  • InvFieldConfigPanel.normalizeTableSelectOptions — 동일하게 양쪽 키 흡수. EntityOptions 호출 시 allTables.length > 0 ? allTables : tables 폴백
  • InvFieldConfigPanel.loadColumnsForTablecolumnName/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.tsfetchUrl useMemo 의존성 배열에 다음 6개 추가:
    • config.entityTable / config.entityValueColumn / config.entityLabelColumn
    • config.ref?.table / config.ref?.valueColumn / config.ref?.displayColumn
  • ref 객체 자체를 dep 으로 넣지 않은 이유: 객체 reference 변동만으로 effect 폭주를 방지하기 위해 내부 키만 추출

디자인 모드 가드:

  • useOptionLoaderisDesignMode === trueneedsFetch = 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.tsOptionLoaderConfig.filters?: OptionFilter[] 추가
  • OptionUserContext 타입 신규 — companyCode / userId / deptCode / userName
  • UseOptionLoaderArgs.userContext 추가 — value_type === "user" 필터 치환용
  • resolveFilters(filters, formData, userContext) 헬퍼 — 치환 규칙:
    • column trim 후 빈 값 → skip
    • value_type=staticf.value 그대로
    • value_type=fieldformData[field_ref], 값 없으면 skip
    • value_type=useruserContext[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 === trueneedsFetch=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 / readonlylockEdit 통합 (선택/삭제/추가 모두 차단)
    • 같은 파일 재선택 가능하도록 hidden input value 매번 초기화
  • frontend/lib/registry/components/input/types.tsInputConfigmaxFiles / 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 / imgWEB_TYPE_V2_MAPPING 항목 → componentType: "input" 으로 변경
    • config triple: { kind: "attach", type: "file", format: "file" | "image", accept, multiple, maxFiles, showPreview }
    • WEB_TYPE_COMPONENT_MAPPINGfile/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 흡수, defaultConfigdefault_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 생성되는지 예시:

// 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.handleSaveActionmasterFormData 구성 단계 보강
  • 파일 컬럼 식별 (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.tsxFileUploadComponent import 제거, isFileComponent import 제거, if (isFileComponent(comp)) return renderFileComponent(comp) 분기 제거, renderFileComponent 함수 통째 제거, uploadFilesAndCreateData 직접 import 제거 (uploadFiles 동적 import 가 master save 안에서 대체)
  • lib/registry/components/index.tsfile-upload/FileUploadRenderer, image-widget/ImageWidgetRenderer, image-display/ImageDisplayRenderer, v2-media/V2MediaRenderer, v2-file-upload/V2FileUploadRenderer auto-register import 5개 모두 제거
  • lib/utils/getComponentConfigPanel.tsxCONFIG_PANEL_MAPv2-media / file-upload / image-display / image-widget mapping 4개 제거
  • components/screen/panels/V2PropertiesPanel.tsxv2ConfigPanelsv2-media 매핑 제거
  • lib/schemas/componentConfig.tsv2MediaOverridesSchema 정의 + componentOverridesRegistryv2-media 등록 + componentDefaultsRegistryv2-media 기본값 모두 제거
  • components/v2/registerV2Components.tsV2Media / V2MediaConfigPanel import + v2-media ComponentDefinition 등록 제거
  • components/v2/V2ComponentRenderer.tsxisV2Media / V2Media import + if (isV2Media(props)) return <V2Media> 분기 제거
  • components/v2/V2ComponentsDemo.tsxV2Media import + media TabsTrigger + media TabsContent 통째 제거
  • components/v2/index.tsV2Media export + V2Media 타입 re-export (V2MediaType / V2MediaConfig / V2MediaProps) 제거
  • components/v2/config-panels/index.tsV2MediaConfigPanel export 제거
  • types/v2-components.tsV2MediaType / 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.tsxcompType?.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.tsxGlobalFileViewer / FileAttachmentDetailModal 의존
  • lib/registry/components/file-upload/FileManagerModal.tsx — FileViewerModal 사용
  • lib/registry/components/file-upload/types.tsFileInfo / 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.tsImageWidget import / export / getWidgetComponentByNamecase "ImageWidget" / getWidgetComponentByWebType 의 image/img/picture/photo 분기 / WebTypeComponentsimage 매핑 모두 제거

잔존 grep 매치 분류 (모두 운영 영향 없음):

  • 폐기 안내 주석 / 작업 노트
  • WidgetRenderer.tsx:41isImageWidget 변수 — widgetType 문자열 (image/img/picture/photo) 매칭만. ImageWidget 본체 import 없이 wrapper pointer-events-none 처리, 실제 렌더는 DynamicWebTypeRenderer → canonical input (Phase D.4)
  • pop-components/pop-text.tsxImageDisplay — 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 메서드 신규 (두 테이블 처리)
    • resetSequencesetCurrentSequenceInRule 사용으로 변경
    • 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.tsxlockedColumn?: { 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

// 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:

// 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) : parentValueformData[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.tsxSwapPicker 신규 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 > maxSelectslice(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 참고.