[agent-pipeline] pipe-20260311225813-8hmk round-1

This commit is contained in:
DDD1542
2026-03-12 08:18:34 +09:00
parent db3ad9d639
commit bb442f5478
3 changed files with 782 additions and 608 deletions

View File

@@ -9,6 +9,7 @@ import React, { useState, useEffect, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
@@ -57,6 +58,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
const [titleColumnOpen, setTitleColumnOpen] = useState(false);
const [descriptionColumnOpen, setDescriptionColumnOpen] = useState(false);
const [titleDescOpen, setTitleDescOpen] = useState(true);
const [styleOpen, setStyleOpen] = useState(false);
const [interactionOpen, setInteractionOpen] = useState(false);
@@ -353,245 +355,264 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
/>
{/* ─── 4단계: 아이템 제목/설명 ─── */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-medium"> /</p>
<Switch
checked={config.showItemTitle ?? false}
onCheckedChange={(checked) => onChange({ showItemTitle: checked })}
/>
</div>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
{config.showItemTitle && (
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
{/* 제목 컬럼 Combobox */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<Popover open={titleColumnOpen} onOpenChange={setTitleColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={titleColumnOpen}
className="h-7 w-full justify-between text-xs font-normal"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: config.titleColumn
? availableColumns.find(c => c.columnName === config.titleColumn)?.displayName || config.titleColumn
: "제목 컬럼 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center"> </CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
onChange({ titleColumn: "" });
setTitleColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", !config.titleColumn ? "opacity-100" : "opacity-0")} />
</CommandItem>
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={col.columnName}
onSelect={() => {
onChange({ titleColumn: col.columnName });
setTitleColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.titleColumn === col.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span>{col.displayName || col.columnName}</span>
{col.displayName && col.displayName !== col.columnName && (
<span className="text-[10px] text-muted-foreground">{col.columnName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 설명 컬럼 Combobox */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> ()</span>
<Popover open={descriptionColumnOpen} onOpenChange={setDescriptionColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={descriptionColumnOpen}
className="h-7 w-full justify-between text-xs font-normal"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: config.descriptionColumn
? availableColumns.find(c => c.columnName === config.descriptionColumn)?.displayName || config.descriptionColumn
: "설명 컬럼 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center"> </CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
onChange({ descriptionColumn: "" });
setDescriptionColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", !config.descriptionColumn ? "opacity-100" : "opacity-0")} />
</CommandItem>
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={col.columnName}
onSelect={() => {
onChange({ descriptionColumn: col.columnName });
setDescriptionColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.descriptionColumn === col.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span>{col.displayName || col.columnName}</span>
{col.displayName && col.displayName !== col.columnName && (
<span className="text-[10px] text-muted-foreground">{col.columnName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 제목 템플릿 (titleColumn 미사용 시 대체) */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> 릿 ()</span>
<Input
value={config.itemTitleTemplate || ""}
onChange={(e) => onChange({ itemTitleTemplate: e.target.value })}
placeholder="{field_name} - {field_code}"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
{/* 제목 스타일 */}
<div className="space-y-2 pt-1">
<span className="text-[10px] text-muted-foreground"> </span>
<div className="grid grid-cols-3 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.titleFontSize || "14px"}
onValueChange={(value) => onChange({ titleFontSize: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="12px">12px</SelectItem>
<SelectItem value="14px">14px</SelectItem>
<SelectItem value="16px">16px</SelectItem>
<SelectItem value="18px">18px</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Input
type="color"
value={config.titleColor || "#374151"}
onChange={(e) => onChange({ titleColor: e.target.value })}
className="h-7"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.titleFontWeight || "600"}
onValueChange={(value) => onChange({ titleFontWeight: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="400"></SelectItem>
<SelectItem value="500"></SelectItem>
<SelectItem value="600"></SelectItem>
<SelectItem value="700"> </SelectItem>
</SelectContent>
</Select>
</div>
<Collapsible open={titleDescOpen} onOpenChange={setTitleDescOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Type className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> /</span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.showItemTitle ? "ON" : "OFF"}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", titleDescOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-3 space-y-3">
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm">/ </p>
<p className="text-[11px] text-muted-foreground"> </p>
</div>
<Switch
checked={config.showItemTitle ?? false}
onCheckedChange={(checked) => onChange({ showItemTitle: checked })}
/>
</div>
</div>
{config.descriptionColumn && (
<div className="space-y-2">
<span className="text-[10px] text-muted-foreground"> </span>
<div className="grid grid-cols-2 gap-2">
{config.showItemTitle && (
<>
{/* 제목 컬럼 Combobox */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.descriptionFontSize || "12px"}
onValueChange={(value) => onChange({ descriptionFontSize: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10px">10px</SelectItem>
<SelectItem value="12px">12px</SelectItem>
<SelectItem value="14px">14px</SelectItem>
</SelectContent>
</Select>
<span className="text-xs text-muted-foreground"> </span>
<Popover open={titleColumnOpen} onOpenChange={setTitleColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={titleColumnOpen}
className="h-7 w-full justify-between text-xs font-normal"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: config.titleColumn
? availableColumns.find(c => c.columnName === config.titleColumn)?.displayName || config.titleColumn
: "제목 컬럼 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center"> </CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
onChange({ titleColumn: "" });
setTitleColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", !config.titleColumn ? "opacity-100" : "opacity-0")} />
</CommandItem>
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={col.columnName}
onSelect={() => {
onChange({ titleColumn: col.columnName });
setTitleColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.titleColumn === col.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="truncate">{col.displayName || col.columnName}</span>
{col.displayName && col.displayName !== col.columnName && (
<span className="truncate text-[10px] text-muted-foreground">{col.columnName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 설명 컬럼 Combobox */}
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<span className="text-xs text-muted-foreground"> ()</span>
<Popover open={descriptionColumnOpen} onOpenChange={setDescriptionColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={descriptionColumnOpen}
className="h-7 w-full justify-between text-xs font-normal"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: config.descriptionColumn
? availableColumns.find(c => c.columnName === config.descriptionColumn)?.displayName || config.descriptionColumn
: "설명 컬럼 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center"> </CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() => {
onChange({ descriptionColumn: "" });
setDescriptionColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", !config.descriptionColumn ? "opacity-100" : "opacity-0")} />
</CommandItem>
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={col.columnName}
onSelect={() => {
onChange({ descriptionColumn: col.columnName });
setDescriptionColumnOpen(false);
}}
className="text-xs"
>
<Check className={cn("mr-2 h-3 w-3", config.descriptionColumn === col.columnName ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="truncate">{col.displayName || col.columnName}</span>
{col.displayName && col.displayName !== col.columnName && (
<span className="truncate text-[10px] text-muted-foreground">{col.columnName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 제목 템플릿 (titleColumn 미사용 시 대체) */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> 릿 ()</span>
<Input
type="color"
value={config.descriptionColor || "#6b7280"}
onChange={(e) => onChange({ descriptionColor: e.target.value })}
className="h-7"
value={config.itemTitleTemplate || ""}
onChange={(e) => onChange({ itemTitleTemplate: e.target.value })}
placeholder="{field_name} - {field_code}"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
</div>
</div>
)}
</div>
)}
{/* 제목 스타일 */}
<div className="space-y-2 pt-1">
<span className="text-[10px] text-muted-foreground"> </span>
<div className="grid grid-cols-3 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.titleFontSize || "14px"}
onValueChange={(value) => onChange({ titleFontSize: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="12px">12px</SelectItem>
<SelectItem value="14px">14px</SelectItem>
<SelectItem value="16px">16px</SelectItem>
<SelectItem value="18px">18px</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Input
type="color"
value={config.titleColor || "#374151"}
onChange={(e) => onChange({ titleColor: e.target.value })}
className="h-7"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.titleFontWeight || "600"}
onValueChange={(value) => onChange({ titleFontWeight: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="400"></SelectItem>
<SelectItem value="500"></SelectItem>
<SelectItem value="600"></SelectItem>
<SelectItem value="700"> </SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{config.descriptionColumn && (
<div className="space-y-2">
<span className="text-[10px] text-muted-foreground"> </span>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Select
value={config.descriptionFontSize || "12px"}
onValueChange={(value) => onChange({ descriptionFontSize: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10px">10px</SelectItem>
<SelectItem value="12px">12px</SelectItem>
<SelectItem value="14px">14px</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<Input
type="color"
value={config.descriptionColor || "#6b7280"}
onChange={(e) => onChange({ descriptionColor: e.target.value })}
className="h-7"
/>
</div>
</div>
</div>
)}
</>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 5단계: 카드 스타일 (Collapsible) ─── */}
<Collapsible open={styleOpen} onOpenChange={setStyleOpen}>
@@ -603,6 +624,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">6</Badge>
</div>
<ChevronDown
className={cn(
@@ -613,10 +635,10 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"></span>
<span className="truncate text-xs text-muted-foreground"></span>
<Input
type="color"
value={config.backgroundColor || "#ffffff"}
@@ -625,7 +647,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"></span>
<span className="truncate text-xs text-muted-foreground"></span>
<Input
value={config.borderRadius || "8px"}
onChange={(e) => onChange({ borderRadius: e.target.value })}
@@ -636,7 +658,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<span className="truncate text-xs text-muted-foreground"> </span>
<Input
value={config.padding || "16px"}
onChange={(e) => onChange({ padding: e.target.value })}
@@ -644,7 +666,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<span className="truncate text-xs text-muted-foreground"> </span>
<Input
value={config.itemHeight || "auto"}
onChange={(e) => onChange({ itemHeight: e.target.value })}
@@ -688,6 +710,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> & </span>
<Badge variant="secondary" className="text-[10px] h-5">7</Badge>
</div>
<ChevronDown
className={cn(
@@ -698,7 +721,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
@@ -868,6 +891,7 @@ function SlotChildrenSection({
}: SlotChildrenSectionProps) {
const [columnComboboxOpen, setColumnComboboxOpen] = useState(false);
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [slotFieldsOpen, setSlotFieldsOpen] = useState(true);
const children = config.children || [];
@@ -930,17 +954,31 @@ function SlotChildrenSection({
};
return (
<>
<div className="space-y-2">
<p className="text-sm font-medium"> </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<Collapsible open={slotFieldsOpen} onOpenChange={setSlotFieldsOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Type className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{children.length}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", slotFieldsOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
<p className="text-[11px] text-muted-foreground">
</p>
{children.length > 0 ? (
<div className="space-y-2">
{children.map((child, index) => {
{children.length > 0 ? (
<div className="max-h-[250px] space-y-2 overflow-y-auto">
{children.map((child, index) => {
const isExpanded = expandedIds.has(child.id);
return (
<div
@@ -951,11 +989,11 @@ function SlotChildrenSection({
<div className="flex h-5 w-5 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary">
{index + 1}
</div>
<div className="flex-1">
<div className="text-xs font-medium">
<div className="flex-1 min-w-0">
<div className="truncate text-xs font-medium">
{child.label || child.fieldName}
</div>
<div className="text-[10px] text-muted-foreground">
<div className="truncate text-[10px] text-muted-foreground">
: {child.fieldName}
</div>
</div>
@@ -1090,81 +1128,83 @@ function SlotChildrenSection({
);
})}
</div>
) : (
<div className="rounded-lg border border-dashed border-border bg-muted/20 p-4 text-center">
<Type className="mx-auto h-6 w-6 text-muted-foreground/50" />
<div className="mt-2 text-xs text-muted-foreground"> </div>
<div className="text-[10px] text-muted-foreground">
</div>
</div>
)}
) : (
<div className="rounded-lg border border-dashed border-border bg-muted/20 p-4 text-center">
<Type className="mx-auto h-6 w-6 text-muted-foreground/50" />
<div className="mt-2 text-xs text-muted-foreground"> </div>
<div className="text-[10px] text-muted-foreground">
</div>
</div>
)}
{/* 컬럼 추가 Combobox */}
<Popover open={columnComboboxOpen} onOpenChange={setColumnComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={columnComboboxOpen}
className="h-8 w-full justify-between text-xs"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: availableColumns.length === 0
? "테이블을 먼저 선택하세요"
: "컬럼 추가..."}
<Plus className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs"> </CommandEmpty>
<CommandGroup heading="사용 가능한 컬럼">
{availableColumns.map((col) => {
const isAdded = children.some((c) => c.fieldName === col.columnName);
return (
<CommandItem
key={col.columnName}
value={`${col.columnName} ${col.displayName || ""}`}
onSelect={() => {
if (!isAdded) {
addComponent(col.columnName, col.displayName || col.columnName);
}
}}
disabled={isAdded}
className={cn(
"text-xs cursor-pointer",
isAdded && "opacity-50 cursor-not-allowed"
)}
>
<Plus
className={cn(
"mr-2 h-3 w-3",
isAdded ? "text-primary" : "text-muted-foreground"
)}
/>
<div className="flex-1">
<div className="font-medium">{col.displayName || col.columnName}</div>
<div className="text-[10px] text-muted-foreground">
{col.columnName}
</div>
</div>
{isAdded && (
<Check className="h-3 w-3 text-primary" />
)}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</>
{/* 컬럼 추가 Combobox */}
<Popover open={columnComboboxOpen} onOpenChange={setColumnComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={columnComboboxOpen}
className="h-8 w-full justify-between text-xs"
disabled={loadingColumns || availableColumns.length === 0}
>
{loadingColumns
? "로딩 중..."
: availableColumns.length === 0
? "테이블을 먼저 선택하세요"
: "컬럼 추가..."}
<Plus className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs"> </CommandEmpty>
<CommandGroup heading="사용 가능한 컬럼">
{availableColumns.map((col) => {
const isAdded = children.some((c) => c.fieldName === col.columnName);
return (
<CommandItem
key={col.columnName}
value={`${col.columnName} ${col.displayName || ""}`}
onSelect={() => {
if (!isAdded) {
addComponent(col.columnName, col.displayName || col.columnName);
}
}}
disabled={isAdded}
className={cn(
"text-xs cursor-pointer",
isAdded && "opacity-50 cursor-not-allowed"
)}
>
<Plus
className={cn(
"mr-2 h-3 w-3",
isAdded ? "text-primary" : "text-muted-foreground"
)}
/>
<div className="flex-1">
<div className="truncate font-medium">{col.displayName || col.columnName}</div>
<div className="truncate text-[10px] text-muted-foreground">
{col.columnName}
</div>
</div>
{isAdded && (
<Check className="h-3 w-3 text-primary" />
)}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</CollapsibleContent>
</Collapsible>
);
}