From fcae946a3f0c0160edecdbf3bc50326c92a28a51 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 12 Mar 2026 00:41:59 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311151253-nyk7 round-8 --- .../config-panels/V2FileUploadConfigPanel.tsx | 371 ++++++++++++++++++ .../V2TextDisplayConfigPanel.tsx | 304 ++++++++++++++ .../components/v2-file-upload/index.ts | 4 +- .../components/v2-text-display/index.ts | 4 +- 4 files changed, 679 insertions(+), 4 deletions(-) create mode 100644 frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx create mode 100644 frontend/components/v2/config-panels/V2TextDisplayConfigPanel.tsx diff --git a/frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx b/frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx new file mode 100644 index 00000000..9d09df8e --- /dev/null +++ b/frontend/components/v2/config-panels/V2FileUploadConfigPanel.tsx @@ -0,0 +1,371 @@ +"use client"; + +/** + * V2FileUpload 설정 패널 + * 토스식 단계별 UX: 파일 형식(카드선택) -> 제한 설정 -> 동작/표시(Switch) -> 고급 설정(접힘) + */ + +import React, { useState, useMemo, useCallback } from "react"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Settings, + ChevronDown, + FileText, + Image, + Archive, + File, + FileImage, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { FileUploadConfig } from "@/lib/registry/components/v2-file-upload/types"; +import { V2FileUploadDefaultConfig } from "@/lib/registry/components/v2-file-upload/config"; + +const FILE_TYPE_CARDS = [ + { value: "*/*", label: "모든 파일", icon: File, desc: "제한 없음" }, + { value: "image/*", label: "이미지", icon: Image, desc: "JPG, PNG 등" }, + { value: ".pdf,.doc,.docx,.xls,.xlsx", label: "문서", icon: FileText, desc: "PDF, Word, Excel" }, + { value: "image/*,.pdf", label: "이미지+PDF", icon: FileImage, desc: "이미지와 PDF" }, + { value: ".zip,.rar,.7z", label: "압축 파일", icon: Archive, desc: "ZIP, RAR 등" }, +] as const; + +const VARIANT_CARDS = [ + { value: "default", label: "기본", desc: "기본 스타일" }, + { value: "outlined", label: "테두리", desc: "테두리 강조" }, + { value: "filled", label: "채움", desc: "배경 채움" }, +] as const; + +const SIZE_CARDS = [ + { value: "sm", label: "작게" }, + { value: "md", label: "보통" }, + { value: "lg", label: "크게" }, +] as const; + +interface V2FileUploadConfigPanelProps { + config: FileUploadConfig; + onChange: (config: Partial) => void; + screenTableName?: string; +} + +export const V2FileUploadConfigPanel: React.FC = ({ + config: propConfig, + onChange, + screenTableName, +}) => { + const [advancedOpen, setAdvancedOpen] = useState(false); + + const config = useMemo(() => ({ + ...V2FileUploadDefaultConfig, + ...propConfig, + }), [propConfig]); + + const maxSizeMB = useMemo(() => { + return (config.maxSize || 10 * 1024 * 1024) / (1024 * 1024); + }, [config.maxSize]); + + const updateConfig = useCallback(( + field: K, + value: FileUploadConfig[K] + ) => { + const newConfig = { ...config, [field]: value }; + onChange({ [field]: value }); + + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("componentConfigChanged", { + detail: { config: newConfig }, + }) + ); + } + }, [config, onChange]); + + const handleMaxSizeChange = useCallback((value: string) => { + const mb = parseFloat(value) || 10; + updateConfig("maxSize", mb * 1024 * 1024); + }, [updateConfig]); + + return ( +
+ {/* ─── 1단계: 허용 파일 형식 카드 선택 ─── */} +
+

허용 파일 형식

+
+ {FILE_TYPE_CARDS.map((card) => { + const Icon = card.icon; + const isSelected = (config.accept || "*/*") === card.value; + return ( + + ); + })} +
+

업로드 가능한 파일 유형을 선택해요

+
+ + {/* ─── 2단계: 파일 제한 설정 ─── */} +
+

파일 제한

+
+
+ 안내 텍스트 + updateConfig("placeholder", e.target.value)} + placeholder="파일을 선택하세요" + className="h-7 w-[160px] text-xs" + /> +
+
+
+ 최대 크기 (MB) + handleMaxSizeChange(e.target.value)} + className="mt-1 h-7 text-xs" + /> +
+
+ 최대 파일 수 + updateConfig("maxFiles", parseInt(e.target.value) || 10)} + className="mt-1 h-7 text-xs" + /> +
+
+
+
+ + {/* ─── 3단계: 동작 설정 (Switch) ─── */} +
+

동작 설정

+
+
+
+

다중 파일 선택

+

여러 파일을 한 번에 선택할 수 있어요

+
+ updateConfig("multiple", checked)} + /> +
+
+
+

파일 삭제 허용

+

업로드된 파일을 삭제할 수 있어요

+
+ updateConfig("allowDelete", checked)} + /> +
+
+
+

파일 다운로드 허용

+

업로드된 파일을 다운로드할 수 있어요

+
+ updateConfig("allowDownload", checked)} + /> +
+
+
+ + {/* ─── 4단계: 표시 설정 (Switch) ─── */} +
+

표시 설정

+
+
+
+

미리보기 표시

+

이미지 파일의 미리보기를 보여줘요

+
+ updateConfig("showPreview", checked)} + /> +
+
+
+

파일 목록 표시

+

업로드된 파일의 목록을 보여줘요

+
+ updateConfig("showFileList", checked)} + /> +
+
+
+

파일 크기 표시

+

각 파일의 크기를 함께 보여줘요

+
+ updateConfig("showFileSize", checked)} + /> +
+
+
+ + {/* ─── 5단계: 스타일 카드 선택 ─── */} +
+

스타일

+
+
+ 스타일 변형 +
+ {VARIANT_CARDS.map((card) => { + const isSelected = (config.variant || "default") === card.value; + return ( + + ); + })} +
+
+
+ 크기 +
+ {SIZE_CARDS.map((card) => { + const isSelected = (config.size || "md") === card.value; + return ( + + ); + })} +
+
+
+
+ + {/* ─── 6단계: 고급 설정 (기본 접혀있음) ─── */} + + + + + +
+ {/* 도움말 */} +
+ 도움말 + updateConfig("helperText", e.target.value)} + placeholder="안내 문구 입력" + className="h-7 w-[160px] text-xs" + /> +
+ + {/* 필수 입력 */} +
+
+

필수 입력

+

파일 첨부를 필수로 해요

+
+ updateConfig("required", checked)} + /> +
+ + {/* 읽기 전용 */} +
+
+

읽기 전용

+

파일 목록만 볼 수 있어요

+
+ updateConfig("readonly", checked)} + /> +
+ + {/* 비활성화 */} +
+
+

비활성화

+

컴포넌트를 비활성화해요

+
+ updateConfig("disabled", checked)} + /> +
+
+
+
+
+ ); +}; + +V2FileUploadConfigPanel.displayName = "V2FileUploadConfigPanel"; + +export default V2FileUploadConfigPanel; diff --git a/frontend/components/v2/config-panels/V2TextDisplayConfigPanel.tsx b/frontend/components/v2/config-panels/V2TextDisplayConfigPanel.tsx new file mode 100644 index 00000000..82291ebb --- /dev/null +++ b/frontend/components/v2/config-panels/V2TextDisplayConfigPanel.tsx @@ -0,0 +1,304 @@ +"use client"; + +/** + * V2TextDisplay 설정 패널 + * 토스식 단계별 UX: 텍스트 내용 -> 폰트 설정(카드선택) -> 정렬(카드선택) -> 고급 설정(접힘) + */ + +import React, { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Settings, + ChevronDown, + AlignLeft, + AlignCenter, + AlignRight, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { TextDisplayConfig } from "@/lib/registry/components/v2-text-display/types"; + +const FONT_SIZE_CARDS = [ + { value: "12px", label: "작게", preview: "Aa" }, + { value: "14px", label: "보통", preview: "Aa" }, + { value: "18px", label: "크게", preview: "Aa" }, + { value: "24px", label: "제목", preview: "Aa" }, +] as const; + +const FONT_WEIGHT_CARDS = [ + { value: "lighter", label: "얇게" }, + { value: "normal", label: "보통" }, + { value: "bold", label: "굵게" }, +] as const; + +const ALIGN_CARDS = [ + { value: "left", label: "왼쪽", icon: AlignLeft }, + { value: "center", label: "가운데", icon: AlignCenter }, + { value: "right", label: "오른쪽", icon: AlignRight }, +] as const; + +interface V2TextDisplayConfigPanelProps { + config: TextDisplayConfig; + onChange: (config: Partial) => void; +} + +export const V2TextDisplayConfigPanel: React.FC = ({ + config, + onChange, +}) => { + const [advancedOpen, setAdvancedOpen] = useState(false); + + const updateConfig = (field: keyof TextDisplayConfig, value: any) => { + const newConfig = { ...config, [field]: value }; + onChange({ [field]: value }); + + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("componentConfigChanged", { + detail: { config: newConfig }, + }) + ); + } + }; + + return ( +
+ {/* ─── 1단계: 표시 텍스트 ─── */} +
+

표시 텍스트

+ updateConfig("text", e.target.value)} + placeholder="표시할 텍스트를 입력하세요" + className="h-8 text-sm" + /> +

화면에 보여질 텍스트를 입력해요

+
+ + {/* ─── 2단계: 폰트 크기 카드 선택 ─── */} +
+

폰트 크기

+
+ {FONT_SIZE_CARDS.map((card) => { + const isSelected = (config.fontSize || "14px") === card.value; + return ( + + ); + })} +
+
+ 직접 입력 + updateConfig("fontSize", e.target.value)} + placeholder="14px" + className="h-7 w-[100px] text-xs" + /> +
+
+ + {/* ─── 3단계: 폰트 굵기 카드 선택 ─── */} +
+

폰트 굵기

+
+ {FONT_WEIGHT_CARDS.map((card) => { + const isSelected = (config.fontWeight || "normal") === card.value; + return ( + + ); + })} +
+
+ + {/* ─── 4단계: 텍스트 정렬 카드 선택 ─── */} +
+

텍스트 정렬

+
+ {ALIGN_CARDS.map((card) => { + const Icon = card.icon; + const isSelected = (config.textAlign || "left") === card.value; + return ( + + ); + })} +
+
+ + {/* ─── 5단계: 텍스트 색상 ─── */} +
+

텍스트 색상

+
+
+
+
+ + {config.color || "#212121"} + +
+ updateConfig("color", e.target.value)} + className="h-7 w-[60px] cursor-pointer p-0.5" + /> +
+
+
+ + {/* ─── 6단계: 고급 설정 (기본 접혀있음) ─── */} + + + + + +
+ {/* 배경색 */} +
+ 배경색 +
+
+ updateConfig("backgroundColor", e.target.value)} + className="h-7 w-[60px] cursor-pointer p-0.5" + /> +
+
+ + {/* 패딩 */} +
+ 패딩 + updateConfig("padding", e.target.value)} + placeholder="8px" + className="h-7 w-[100px] text-xs" + /> +
+ + {/* 모서리 둥글기 */} +
+ 모서리 둥글기 + updateConfig("borderRadius", e.target.value)} + placeholder="4px" + className="h-7 w-[100px] text-xs" + /> +
+ + {/* 테두리 */} +
+ 테두리 + updateConfig("border", e.target.value)} + placeholder="1px solid #d1d5db" + className="h-7 w-[140px] text-xs" + /> +
+ + {/* 비활성화 */} +
+
+

비활성화

+

+ 컴포넌트를 비활성화 상태로 만들어요 +

+
+ updateConfig("disabled", checked)} + /> +
+
+ + +
+ ); +}; + +V2TextDisplayConfigPanel.displayName = "V2TextDisplayConfigPanel"; + +export default V2TextDisplayConfigPanel; diff --git a/frontend/lib/registry/components/v2-file-upload/index.ts b/frontend/lib/registry/components/v2-file-upload/index.ts index 0bf109bb..1ad56db1 100644 --- a/frontend/lib/registry/components/v2-file-upload/index.ts +++ b/frontend/lib/registry/components/v2-file-upload/index.ts @@ -5,7 +5,7 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition import { ComponentCategory } from "@/types/component"; import type { WebType } from "@/types/screen"; import { FileUploadComponent } from "./FileUploadComponent"; -import { FileUploadConfigPanel } from "./FileUploadConfigPanel"; +import { V2FileUploadConfigPanel } from "@/components/v2/config-panels/V2FileUploadConfigPanel"; import { FileUploadConfig } from "./types"; /** @@ -27,7 +27,7 @@ export const V2FileUploadDefinition = createComponentDefinition({ maxSize: 10 * 1024 * 1024, // 10MB }, defaultSize: { width: 350, height: 240 }, - configPanel: FileUploadConfigPanel, + configPanel: V2FileUploadConfigPanel, icon: "Upload", tags: ["file", "upload", "attachment", "v2"], version: "2.0.0", diff --git a/frontend/lib/registry/components/v2-text-display/index.ts b/frontend/lib/registry/components/v2-text-display/index.ts index d7f1399d..9f361d51 100644 --- a/frontend/lib/registry/components/v2-text-display/index.ts +++ b/frontend/lib/registry/components/v2-text-display/index.ts @@ -5,7 +5,7 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition import { ComponentCategory } from "@/types/component"; import type { WebType } from "@/types/screen"; import { TextDisplayWrapper } from "./TextDisplayComponent"; -import { TextDisplayConfigPanel } from "./TextDisplayConfigPanel"; +import { V2TextDisplayConfigPanel } from "@/components/v2/config-panels/V2TextDisplayConfigPanel"; import { TextDisplayConfig } from "./types"; /** @@ -28,7 +28,7 @@ export const V2TextDisplayDefinition = createComponentDefinition({ textAlign: "left", }, defaultSize: { width: 150, height: 24 }, - configPanel: TextDisplayConfigPanel, + configPanel: V2TextDisplayConfigPanel, icon: "Type", tags: ["텍스트", "표시", "라벨"], version: "1.0.0",