선택항목 상게입력 컴포넌트 구현

This commit is contained in:
kjs
2025-11-17 12:23:45 +09:00
parent 2c099feea0
commit a6e6a14fd1
18 changed files with 2279 additions and 6 deletions

View File

@@ -60,6 +60,9 @@ interface RealtimePreviewProps {
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
// 🆕 조건부 컨테이너 높이 변화 콜백
onHeightChange?: (componentId: string, newHeight: number) => void;
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@@ -123,6 +126,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onFlowRefresh,
formData,
onFormDataChange,
onHeightChange, // 🆕 조건부 컨테이너 높이 변화 콜백
}) => {
const [actualHeight, setActualHeight] = React.useState<number | null>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
@@ -225,6 +229,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
};
const getHeight = () => {
// 🆕 조건부 컨테이너는 높이를 자동으로 설정 (내용물에 따라 자동 조정)
const isConditionalContainer = (component as any).componentType === "conditional-container";
if (isConditionalContainer && !isDesignMode) {
return "auto"; // 런타임에서는 내용물 높이에 맞춤
}
// 플로우 위젯의 경우 측정된 높이 사용
const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget";
if (isFlowWidget && actualHeight) {
@@ -325,7 +335,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
(contentRef as any).current = node;
}
}}
className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} overflow-visible`}
className={`${
(component.type === "component" && (component as any).componentType === "flow-widget") ||
((component as any).componentType === "conditional-container" && !isDesignMode)
? "h-auto"
: "h-full"
} overflow-visible`}
style={{ width: "100%", maxWidth: "100%" }}
>
<DynamicComponentRenderer
@@ -361,6 +376,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
sortBy={sortBy}
sortOrder={sortOrder}
columnOrder={columnOrder}
onHeightChange={onHeightChange}
/>
</div>

View File

@@ -274,6 +274,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
<SelectItem value="edit"></SelectItem>
<SelectItem value="copy"> ( )</SelectItem>
<SelectItem value="navigate"> </SelectItem>
<SelectItem value="openModalWithData"> + 🆕</SelectItem>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="control"> </SelectItem>
<SelectItem value="view_table_history"> </SelectItem>
@@ -409,6 +410,136 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div>
)}
{/* 🆕 데이터 전달 + 모달 열기 액션 설정 */}
{component.componentConfig?.action?.type === "openModalWithData" && (
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4 dark:bg-blue-950/20">
<h4 className="text-sm font-medium text-foreground"> + </h4>
<p className="text-xs text-muted-foreground">
TableList에서
</p>
<div>
<Label htmlFor="data-source-id"> ID</Label>
<Input
id="data-source-id"
placeholder="예: item_info (테이블명과 동일하게 입력)"
value={component.componentConfig?.action?.dataSourceId || ""}
onChange={(e) => {
onUpdateProperty("componentConfig.action.dataSourceId", e.target.value);
}}
/>
<p className="mt-1 text-xs text-muted-foreground">
TableList에서 ID와 ( )
</p>
</div>
<div>
<Label htmlFor="modal-title-with-data"> </Label>
<Input
id="modal-title-with-data"
placeholder="예: 상세 정보 입력"
value={localInputs.modalTitle}
onChange={(e) => {
const newValue = e.target.value;
setLocalInputs((prev) => ({ ...prev, modalTitle: newValue }));
onUpdateProperty("componentConfig.action.modalTitle", newValue);
}}
/>
</div>
<div>
<Label htmlFor="modal-size-with-data"> </Label>
<Select
value={component.componentConfig?.action?.modalSize || "lg"}
onValueChange={(value) => {
onUpdateProperty("componentConfig.action.modalSize", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="모달 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm"> (Small)</SelectItem>
<SelectItem value="md"> (Medium)</SelectItem>
<SelectItem value="lg"> (Large) - </SelectItem>
<SelectItem value="xl"> (Extra Large)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="target-screen-with-data"> </Label>
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === parseInt(config.action?.targetScreenId))?.name ||
"화면을 선택하세요..."
: "화면을 선택하세요..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col">
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="화면 검색..."
value={modalSearchTerm}
onChange={(e) => setModalSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(modalSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-muted-foreground"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-muted-foreground"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`modal-data-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
onClick={() => {
onUpdateProperty("componentConfig.action.targetScreenId", screen.id);
setModalScreenOpen(false);
setModalSearchTerm("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
parseInt(config.action?.targetScreenId) === screen.id ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.name}</span>
{screen.description && <span className="text-xs text-muted-foreground">{screen.description}</span>}
</div>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-muted-foreground">
SelectedItemsDetailInput
</p>
</div>
</div>
)}
{/* 수정 액션 설정 */}
{(component.componentConfig?.action?.type || "save") === "edit" && (
<div className="mt-4 space-y-4 rounded-lg border bg-success/10 p-4">

View File

@@ -7,6 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Separator } from "@/components/ui/separator";
import { Plus, X, GripVertical, ChevronDown, ChevronUp } from "lucide-react";
import { RepeaterFieldGroupConfig, RepeaterData, RepeaterItemData, RepeaterFieldDefinition } from "@/types/repeater";
import { cn } from "@/lib/utils";