From cf97db7fbf949b744587486663971b20c2a88dec Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 18:44:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(universal-form-modal):=20Select=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=A7=81=EC=A0=91=20=EC=9E=85=EB=A0=A5(Co?= =?UTF-8?q?mbobox)=20=EB=AA=A8=EB=93=9C=20=EC=B6=94=EA=B0=80=20SelectOptio?= =?UTF-8?q?nConfig=EC=97=90=20allowCustomInput=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20FieldDetailSettingsModal=EC=97=90=20"?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=9E=85=EB=A0=A5=20=ED=97=88=EC=9A=A9"?= =?UTF-8?q?=20Switch=20UI=20=EC=B6=94=EA=B0=80=20CascadingSelectField?= =?UTF-8?q?=EC=97=90=20Combobox=20=EB=AA=A8=EB=93=9C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(Popover+Command)=20SelectField=EC=97=90=20Combobox=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EA=B5=AC=ED=98=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EA=B3=BC=20=EC=A7=81=EC=A0=91=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=8F=99=EC=8B=9C=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UniversalFormModalComponent.tsx | 170 +++++++++++++++++- .../modals/FieldDetailSettingsModal.tsx | 25 +++ .../components/universal-form-modal/types.ts | 5 + 3 files changed, 199 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index e6976844..662f6379 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -19,7 +19,9 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { ChevronDown, ChevronUp, ChevronRight, Plus, Trash2, Loader2 } from "lucide-react"; +import { ChevronDown, ChevronUp, ChevronRight, Plus, Trash2, Loader2, Check, ChevronsUpDown } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -42,6 +44,7 @@ import { TableSectionRenderer } from "./TableSectionRenderer"; /** * ๐Ÿ”— ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด Select ํ•„๋“œ ์ปดํฌ๋„ŒํŠธ + * allowCustomInput์ด true์ด๋ฉด Combobox ํ˜•ํƒœ๋กœ ์ง์ ‘ ์ž…๋ ฅ ๊ฐ€๋Šฅ */ interface CascadingSelectFieldProps { fieldId: string; @@ -51,6 +54,7 @@ interface CascadingSelectFieldProps { onChange: (value: string) => void; placeholder?: string; disabled?: boolean; + allowCustomInput?: boolean; } const CascadingSelectField: React.FC = ({ @@ -61,12 +65,20 @@ const CascadingSelectField: React.FC = ({ onChange, placeholder, disabled, + allowCustomInput = false, }) => { + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(value || ""); const { options, loading } = useCascadingDropdown({ config, parentValue, }); + // value๊ฐ€ ์™ธ๋ถ€์—์„œ ๋ณ€๊ฒฝ๋˜๋ฉด inputValue๋„ ๋™๊ธฐํ™” + useEffect(() => { + setInputValue(value || ""); + }, [value]); + const getPlaceholder = () => { if (!parentValue) { return config.emptyParentMessage || "์ƒ์œ„ ํ•ญ๋ชฉ์„ ๋จผ์ € ์„ ํƒํ•˜์„ธ์š”"; @@ -82,6 +94,79 @@ const CascadingSelectField: React.FC = ({ const isDisabled = disabled || !parentValue || loading; + // Combobox ํ˜•ํƒœ (์ง์ ‘ ์ž…๋ ฅ ํ—ˆ์šฉ) + if (allowCustomInput) { + return ( + + +
+ { + setInputValue(e.target.value); + onChange(e.target.value); + }} + placeholder={getPlaceholder()} + disabled={isDisabled} + className="w-full pr-8" + /> + +
+
+ + + + + + {!parentValue + ? config.emptyParentMessage || "์ƒ์œ„ ํ•ญ๋ชฉ์„ ๋จผ์ € ์„ ํƒํ•˜์„ธ์š”" + : config.noOptionsMessage || "์„ ํƒ ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค"} + + + {options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + { + setInputValue(option.label); + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + +
+ ); + } + + // ๊ธฐ๋ณธ Select ํ˜•ํƒœ (๋ชฉ๋ก์—์„œ๋งŒ ์„ ํƒ) return ( { + setInputValue(e.target.value); + onChange(e.target.value); + }} + placeholder={loading ? "๋กœ๋”ฉ ์ค‘..." : placeholder} + disabled={disabled || loading} + className="w-full pr-8" + /> + + + + + + + + ์„ ํƒ ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค + + {options + .filter((option) => option.value && option.value !== "") + .map((option) => ( + { + setInputValue(option.label); + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + + + ); + } + + // ๊ธฐ๋ณธ Select ํ˜•ํƒœ (๋ชฉ๋ก์—์„œ๋งŒ ์„ ํƒ) return (