From 4a8413000bb818be1bbb0ffae24871ebd4e53f18 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 12 May 2026 18:36:43 +0900 Subject: [PATCH] Consolidate canonical input migration Remove legacy v2 input/select and file/media runtimes, add canonical option/file loaders, and document Codex handoff. --- .../app/(main)/screens/[screenId]/page.tsx | 7 +- .../numbering-rule/AutoConfigPanel.tsx | 4 +- .../screen/InteractiveScreenViewerDynamic.tsx | 247 ++- frontend/components/screen/ScreenDesigner.tsx | 13 +- .../components/screen/ScreenSettingModal.tsx | 16 +- .../screen/panels/ComponentsPanel.tsx | 26 +- .../screen/panels/V2PropertiesPanel.tsx | 88 +- .../screen/widgets/types/ImageWidget.tsx | 205 --- .../components/screen/widgets/types/index.ts | 15 +- frontend/components/v2/DynamicConfigPanel.tsx | 85 +- .../components/v2/V2ComponentRenderer.tsx | 24 +- frontend/components/v2/V2ComponentsDemo.tsx | 695 +-------- frontend/components/v2/V2Input.tsx | 1286 ---------------- frontend/components/v2/V2Media.tsx | 979 ------------ frontend/components/v2/V2Select.tsx | 1350 ----------------- .../v2/config-panels/InvFieldConfigPanel.tsx | 95 +- .../V2AggregationWidgetConfigPanel.tsx | 3 +- .../config-panels/V2FileUploadConfigPanel.tsx | 371 ----- .../v2/config-panels/V2InputConfigPanel.tsx | 1133 -------------- .../v2/config-panels/V2MediaConfigPanel.tsx | 322 ---- .../v2/config-panels/V2SelectConfigPanel.tsx | 847 ----------- frontend/components/v2/config-panels/index.ts | 5 +- frontend/components/v2/index.ts | 27 +- .../components/v2/registerV2Components.ts | 64 +- frontend/lib/hooks/useDialogAutoValidation.ts | 2 +- .../lib/registry/DynamicComponentRenderer.tsx | 130 +- .../lib/registry/DynamicWebTypeRenderer.tsx | 84 +- .../file-upload/FileUploadComponent.tsx | 1317 ---------------- .../file-upload/FileUploadConfigPanel.tsx | 72 - .../file-upload/FileUploadRenderer.tsx | 56 - .../registry/components/file-upload/README.md | 91 -- .../registry/components/file-upload/config.ts | 40 - .../registry/components/file-upload/index.ts | 50 +- .../image-display/ImageDisplayComponent.tsx | 199 --- .../image-display/ImageDisplayConfigPanel.tsx | 175 --- .../image-display/ImageDisplayRenderer.tsx | 56 - .../components/image-display/README.md | 91 -- .../components/image-display/config.ts | 53 - .../components/image-display/index.ts | 46 - .../components/image-display/types.ts | 50 - .../image-widget/ImageWidgetConfigPanel.tsx | 69 - .../image-widget/ImageWidgetRenderer.tsx | 57 - .../registry/components/image-widget/index.ts | 41 - frontend/lib/registry/components/index.ts | 12 +- .../components/input/InputComponent.tsx | 219 ++- .../registry/components/input/file-picker.tsx | 249 +++ .../lib/registry/components/input/index.ts | 2 +- .../components/input/numbering-picker.tsx | 2 +- .../components/input/select-pickers.tsx | 204 ++- .../lib/registry/components/input/types.ts | 8 +- .../components/input/use-option-loader.ts | 530 +++++++ .../AggregationWidgetConfigPanel.tsx | 4 +- .../ButtonPrimaryComponent.tsx | 2 +- .../v2-file-upload/FileManagerModal.tsx | 494 ------ .../v2-file-upload/FileUploadComponent.tsx | 1198 --------------- .../v2-file-upload/FileUploadConfigPanel.tsx | 287 ---- .../v2-file-upload/FileViewerModal.tsx | 545 ------- .../v2-file-upload/V2FileUploadRenderer.tsx | 56 - .../components/v2-file-upload/config.ts | 62 - .../components/v2-file-upload/index.ts | 47 - .../components/v2-file-upload/types.ts | 114 -- .../components/v2-input/V2InputRenderer.tsx | 134 -- .../lib/registry/components/v2-input/index.ts | 44 - .../components/v2-media/V2MediaRenderer.tsx | 136 -- .../lib/registry/components/v2-media/index.ts | 33 - .../components/v2-select/V2SelectRenderer.tsx | 156 -- .../registry/components/v2-select/index.ts | 44 - .../lib/registry/hoc/withContainerQuery.css | 27 +- frontend/lib/schemas/componentConfig.ts | 51 +- .../lib/utils/getComponentConfigPanel.tsx | 11 +- frontend/lib/utils/templateMigrate.ts | 3 +- frontend/lib/utils/webTypeMapping.ts | 103 +- frontend/styles/v5-layout.css | 4 +- frontend/types/invyone-component.ts | 26 +- frontend/types/numbering-rule.ts | 2 +- frontend/types/unified-components.ts | 8 +- frontend/types/v2-components.ts | 191 +-- .../2026-05-08-input-canonical-migration.md | 578 ++++++- ...026-05-12-codex-handoff-input-canonical.md | 215 +++ 79 files changed, 2359 insertions(+), 14028 deletions(-) delete mode 100644 frontend/components/screen/widgets/types/ImageWidget.tsx delete mode 100644 frontend/components/v2/V2Input.tsx delete mode 100644 frontend/components/v2/V2Media.tsx delete mode 100644 frontend/components/v2/V2Select.tsx delete mode 100644 frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx delete mode 100644 frontend/components/v2/config-panels/V2InputConfigPanel.tsx delete mode 100644 frontend/components/v2/config-panels/V2MediaConfigPanel.tsx delete mode 100644 frontend/components/v2/config-panels/V2SelectConfigPanel.tsx delete mode 100644 frontend/lib/registry/components/file-upload/FileUploadComponent.tsx delete mode 100644 frontend/lib/registry/components/file-upload/FileUploadConfigPanel.tsx delete mode 100644 frontend/lib/registry/components/file-upload/FileUploadRenderer.tsx delete mode 100644 frontend/lib/registry/components/file-upload/README.md delete mode 100644 frontend/lib/registry/components/file-upload/config.ts delete mode 100644 frontend/lib/registry/components/image-display/ImageDisplayComponent.tsx delete mode 100644 frontend/lib/registry/components/image-display/ImageDisplayConfigPanel.tsx delete mode 100644 frontend/lib/registry/components/image-display/ImageDisplayRenderer.tsx delete mode 100644 frontend/lib/registry/components/image-display/README.md delete mode 100644 frontend/lib/registry/components/image-display/config.ts delete mode 100644 frontend/lib/registry/components/image-display/index.ts delete mode 100644 frontend/lib/registry/components/image-display/types.ts delete mode 100644 frontend/lib/registry/components/image-widget/ImageWidgetConfigPanel.tsx delete mode 100644 frontend/lib/registry/components/image-widget/ImageWidgetRenderer.tsx delete mode 100644 frontend/lib/registry/components/image-widget/index.ts create mode 100644 frontend/lib/registry/components/input/file-picker.tsx create mode 100644 frontend/lib/registry/components/input/use-option-loader.ts delete mode 100644 frontend/lib/registry/components/v2-file-upload/FileManagerModal.tsx delete mode 100644 frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx delete mode 100644 frontend/lib/registry/components/v2-file-upload/FileUploadConfigPanel.tsx delete mode 100644 frontend/lib/registry/components/v2-file-upload/FileViewerModal.tsx delete mode 100644 frontend/lib/registry/components/v2-file-upload/V2FileUploadRenderer.tsx delete mode 100644 frontend/lib/registry/components/v2-file-upload/config.ts delete mode 100644 frontend/lib/registry/components/v2-file-upload/index.ts delete mode 100644 frontend/lib/registry/components/v2-file-upload/types.ts delete mode 100644 frontend/lib/registry/components/v2-input/V2InputRenderer.tsx delete mode 100644 frontend/lib/registry/components/v2-input/index.ts delete mode 100644 frontend/lib/registry/components/v2-media/V2MediaRenderer.tsx delete mode 100644 frontend/lib/registry/components/v2-media/index.ts delete mode 100644 frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx delete mode 100644 frontend/lib/registry/components/v2-select/index.ts create mode 100644 notes/gbpark/2026-05-12-codex-handoff-input-canonical.md diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index ec98a6ed..99aba929 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -442,11 +442,8 @@ function ScreenViewPage({ screenIdProp, menuObjidProp }: ScreenViewPageProps = { const isInputType = compType?.includes("input") || compType?.includes("select") || - compType?.includes("textarea") || - compType?.includes("v2-input") || - compType?.includes("v2-select") || - compType?.includes("v2-media") || - compType?.includes("file-upload"); + compType?.includes("textarea"); + // (옛 v2-media / file-upload 매칭은 Phase D.5 에서 제거 — canonical input 의 includes("input") 으로 포함) const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx index 6fa2bf46..d5e44305 100644 --- a/frontend/components/numbering-rule/AutoConfigPanel.tsx +++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx @@ -699,7 +699,7 @@ const CategoryConfigPanel: React.FC = ({ return { valueId: selectedId, - valueCode: node.value_code, // value_code 추가 (V2Select 호환) + valueCode: node.value_code, // value_code 추가 (canonical input 의 카테고리 옵션 매칭용) valueLabel: node.value_label, valuePath: pathParts.join(" > "), }; @@ -713,7 +713,7 @@ const CategoryConfigPanel: React.FC = ({ const newMapping: CategoryFormatMapping = { category_value_id: selectedInfo.valueId, - category_value_code: selectedInfo.valueCode, // V2Select에서 valueCode를 value로 사용하므로 매칭용 저장 + category_value_code: selectedInfo.valueCode, // canonical input 의 카테고리 옵션이 valueCode 를 value 로 사용하므로 매칭용 저장 category_value_label: selectedInfo.valueLabel, category_value_path: selectedInfo.valuePath, format: newFormat.trim(), diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index af441be7..ac8321e6 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -5,15 +5,15 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAuth } from "@/hooks/useAuth"; -import { uploadFilesAndCreateData } from "@/lib/api/file"; +// uploadFilesAndCreateData 직접 import 제거 (Phase D.5) — master save 의 uploadFiles 동적 import 가 대체 import { toast } from "sonner"; -import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, ButtonTypeConfig } from "@/types/screen"; -import { FileUploadComponent } from "@/lib/registry/components/file-upload/FileUploadComponent"; +import { ComponentData, WidgetComponent, DataTableComponent, ButtonTypeConfig } from "@/types/screen"; +// 옛 FileUploadComponent 직접 import 는 Phase D.5 에서 제거 — canonical input 의 FilePicker 가 처리. import { InteractiveDataTable } from "./InteractiveDataTable"; import { DynamicWebTypeRenderer } from "@/lib/registry"; import { DynamicComponentRenderer, isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; -import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils"; +import { isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils"; import { FlowButtonGroup } from "./widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/screen-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; @@ -406,10 +406,10 @@ export const InteractiveScreenViewerDynamic: React.FC = {}; - - // 파일 업로드 컴포넌트의 columnName 목록 수집 (v2-media, file-upload 모두 포함) - const mediaColumnNames = new Set( - allComponents - .filter((c: any) => - c.componentType === "v2-media" || - c.componentType === "file-upload" || - c.url?.includes("v2-media") || - c.url?.includes("file-upload") - ) - .map((c: any) => c.column_name || c.componentConfig?.column_name) - .filter(Boolean) - ); - - Object.entries(formData).forEach(([key, value]) => { + + // 파일 컬럼 수집: + // - 옛 v2-media / file-upload (Phase D.5 폐기 전 호환) + // - canonical input 의 file 분기 (componentType === "input" + config.type === "file" + // 또는 config.kind === "attach" 또는 config.format ∈ {file, image, doc}) + const isFileColumnComponent = (c: any): boolean => { + const t = c.componentType || c.config?.componentType || c.overrides?.type; + if (t === "v2-media" || t === "file-upload") return true; + if (c.url?.includes("v2-media") || c.url?.includes("file-upload")) return true; + if (t === "input") { + const cc = c.componentConfig || c.config || c.overrides || {}; + if (cc.type === "file") return true; + if (cc.kind === "attach") return true; + if (cc.format === "file" || cc.format === "image" || cc.format === "doc") return true; + } + return false; + }; + const fileComponentByColumn = new Map(); + for (const c of allComponents as any[]) { + if (!isFileColumnComponent(c)) continue; + const col = + c.column_name || + c.columnName || + c.componentConfig?.column_name || + c.componentConfig?.columnName || + c.config?.column_name || + c.config?.columnName || + c.overrides?.column_name || + c.overrides?.columnName; + if (col) fileComponentByColumn.set(col, c); + } + + // canonical input file 값 변환: + // - File / File[] → uploadFiles 호출 후 응답 FileInfo[].id (또는 server_path) 로 치환 + // - string / string[] → 이미 저장된 값으로 유지 + // - null / undefined / "" → 그대로 + const { uploadFiles: uploadFilesApi } = await import("@/lib/api/file"); + const uploadIfFileValue = async ( + rawValue: unknown, + columnName: string, + comp: any, + ): Promise => { + if (rawValue == null) return rawValue; + const isArr = Array.isArray(rawValue); + const arr = isArr ? (rawValue as unknown[]) : [rawValue]; + // 이미 string 만 있으면 업로드 skip + if (arr.every((v) => typeof v === "string")) { + return rawValue; + } + const files = arr.filter((v): v is File => typeof File !== "undefined" && v instanceof File); + const existingStrings = arr.filter((v): v is string => typeof v === "string"); + if (files.length === 0) { + // File 도 string 도 아닌 값은 그대로 (FileInfo 객체 등) + return rawValue; + } + try { + const cc = comp?.componentConfig || comp?.config || comp?.overrides || {}; + const res = await uploadFilesApi({ + files, + tableName: screenInfo?.tableName, + fieldName: columnName, + columnName: columnName, + recordId: formData.id, + isVirtualFileColumn: !!cc.isVirtualFileColumn, + autoLink: true, + company_code: user?.company_code, + isRecordMode: !!formData.id, + }); + const uploaded = res?.files || res?.data || []; + const uploadedIds = uploaded + .map((f: any) => f?.id ?? f?.server_path ?? f?.server_filename) + .filter((v: any) => v != null); + if (!isArr) return uploadedIds[0] ?? null; + return [...existingStrings, ...uploadedIds]; + } catch (uploadErr) { + console.error(`[canonical file] 업로드 실패: column=${columnName}`, uploadErr); + // 업로드 실패 시 안전하게 기존 string 만 유지 + return isArr ? existingStrings : (existingStrings[0] ?? null); + } + }; + + for (const [key, value] of Object.entries(formData)) { + const comp = fileComponentByColumn.get(key); + if (comp) { + // 파일 컬럼 — File 객체 업로드 후 id 로 치환 + masterFormData[key] = await uploadIfFileValue(value, key, comp); + continue; + } if (!Array.isArray(value)) { masterFormData[key] = value; - } else if (mediaColumnNames.has(key)) { - masterFormData[key] = value.length > 0 ? value[0] : null; - console.log(`📷 미디어 데이터 저장: ${key}, objid: ${masterFormData[key]}`); } else { console.log(`🔄 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`); } - }); + } const saveData: DynamicFormData = { table_name: screenInfo.tableName, @@ -1000,117 +1070,6 @@ export const InteractiveScreenViewerDynamic: React.FC { - const { label, readonly } = comp; - const fieldName = comp.column_name || comp.id; - - // 화면 ID 추출 (URL에서) - const screenId = - screenInfo?.id || - (typeof window !== "undefined" && window.location.pathname.includes("/screens/") - ? parseInt(window.location.pathname.split("/screens/")[1]) - : null); - - return ( -
- {/* 실제 FileUploadComponent 사용 */} - { - // console.log("📝 실제 화면 파일 업로드 완료:", data); - if (onFormDataChange) { - Object.entries(data).forEach(([key, value]) => { - onFormDataChange(key, value); - }); - } - }} - onUpdate={(updates) => { - console.log("🔄🔄🔄 실제 화면 파일 컴포넌트 업데이트:", { - componentId: comp.id, - hasUploadedFiles: !!updates.uploadedFiles, - filesCount: updates.uploadedFiles?.length || 0, - hasLastFileUpdate: !!updates.lastFileUpdate, - updates, - }); - - // 파일 업로드/삭제 완료 시 formData 업데이터 - if (updates.uploadedFiles && onFormDataChange) { - onFormDataChange(fieldName, updates.uploadedFiles); - } - - // 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두) - if (updates.uploadedFiles !== undefined && typeof window !== "undefined") { - // 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음) - const action = updates.lastFileUpdate ? "update" : "sync"; - - const eventDetail = { - componentId: comp.id, - files: updates.uploadedFiles, - fileCount: updates.uploadedFiles.length, - action: action, - timestamp: updates.lastFileUpdate || Date.now(), - source: "realScreen", // 실제 화면에서 온 이벤트임을 표시 - }; - - // console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail); - - const event = new CustomEvent("globalFileStateChanged", { - detail: eventDetail, - }); - window.dispatchEvent(event); - - // console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료"); - - // 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비) - setTimeout(() => { - // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)"); - window.dispatchEvent( - new CustomEvent("globalFileStateChanged", { - detail: { ...eventDetail, delayed: true }, - }), - ); - }, 100); - - setTimeout(() => { - // console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)"); - window.dispatchEvent( - new CustomEvent("globalFileStateChanged", { - detail: { ...eventDetail, delayed: true, attempt: 2 }, - }), - ); - }, 500); - } - }} - /> -
- ); - }; // 메인 렌더링 const { type, position, size, style = {} } = component; @@ -1121,13 +1080,11 @@ export const InteractiveScreenViewerDynamic: React.FC