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

View File

@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
@ -182,6 +183,14 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
// 🆕 확장된 컬럼 (상세 설정 표시용) // 🆕 확장된 컬럼 (상세 설정 표시용)
const [expandedColumn, setExpandedColumn] = useState<string | null>(null); const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
// Collapsible 상태
const [featureOptionsOpen, setFeatureOptionsOpen] = useState(true);
const [columnSelectOpen, setColumnSelectOpen] = useState(true);
const [selectedColumnsOpen, setSelectedColumnsOpen] = useState(true);
const [calcRulesOpen, setCalcRulesOpen] = useState(false);
const [entityJoinSubOpen, setEntityJoinSubOpen] = useState<Record<number, boolean>>({});
const [configuredJoinsOpen, setConfiguredJoinsOpen] = useState(false);
// 🆕 채번 규칙 목록 // 🆕 채번 규칙 목록
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]); const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingNumberingRules, setLoadingNumberingRules] = useState(false); const [loadingNumberingRules, setLoadingNumberingRules] = useState(false);
@ -1120,72 +1129,86 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
</> </>
)} )}
{/* 기능 옵션 - 토스식 Switch + 설명 */} {/* 기능 옵션 - Collapsible */}
<div className="rounded-lg border bg-muted/30 p-4 space-y-1"> <Collapsible open={featureOptionsOpen} onOpenChange={setFeatureOptionsOpen}>
<span className="text-sm font-medium"> </span> <CollapsibleTrigger asChild>
<div className="space-y-2"> <button
<div className="flex items-center justify-between py-1"> type="button"
<div> 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"
<p className="text-sm"> </p> >
<p className="text-[11px] text-muted-foreground"> </p> <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> </div>
<Switch <ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", featureOptionsOpen && "rotate-180")} />
checked={config.features?.showAddButton ?? true} </button>
onCheckedChange={(checked) => updateFeatures("showAddButton", checked)} </CollapsibleTrigger>
/> <CollapsibleContent>
</div> <div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-3 space-y-1">
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<p className="text-sm"> </p> <p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground"> </p> <p className="text-[11px] text-muted-foreground"> </p>
</div>
<Switch
checked={config.features?.showAddButton ?? true}
onCheckedChange={(checked) => updateFeatures("showAddButton", checked)}
/>
</div> </div>
<Switch <div className="flex items-center justify-between py-1">
checked={config.features?.showDeleteButton ?? true} <div>
onCheckedChange={(checked) => updateFeatures("showDeleteButton", checked)} <p className="text-sm"> </p>
/> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<div className="flex items-center justify-between py-1"> <Switch
<div> checked={config.features?.showDeleteButton ?? true}
<p className="text-sm"> </p> onCheckedChange={(checked) => updateFeatures("showDeleteButton", checked)}
<p className="text-[11px] text-muted-foreground"> </p> />
</div> </div>
<Switch <div className="flex items-center justify-between py-1">
checked={config.features?.inlineEdit ?? false} <div>
onCheckedChange={(checked) => updateFeatures("inlineEdit", checked)} <p className="text-sm"> </p>
/> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<div className="flex items-center justify-between py-1"> <Switch
<div> checked={config.features?.inlineEdit ?? false}
<p className="text-sm"> </p> onCheckedChange={(checked) => updateFeatures("inlineEdit", checked)}
<p className="text-[11px] text-muted-foreground"> </p> />
</div> </div>
<Switch <div className="flex items-center justify-between py-1">
checked={config.features?.multiSelect ?? true} <div>
onCheckedChange={(checked) => updateFeatures("multiSelect", checked)} <p className="text-sm"> </p>
/> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<div className="flex items-center justify-between py-1"> <Switch
<div> checked={config.features?.multiSelect ?? true}
<p className="text-sm"> </p> onCheckedChange={(checked) => updateFeatures("multiSelect", checked)}
<p className="text-[11px] text-muted-foreground"> </p> />
</div> </div>
<Switch <div className="flex items-center justify-between py-1">
checked={config.features?.showRowNumber ?? false} <div>
onCheckedChange={(checked) => updateFeatures("showRowNumber", checked)} <p className="text-sm"> </p>
/> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<div className="flex items-center justify-between py-1"> <Switch
<div> checked={config.features?.showRowNumber ?? false}
<p className="text-sm"> </p> onCheckedChange={(checked) => updateFeatures("showRowNumber", checked)}
<p className="text-[11px] text-muted-foreground"> </p> />
</div>
<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.features?.selectable ?? false}
onCheckedChange={(checked) => updateFeatures("selectable", checked)}
/>
</div> </div>
<Switch
checked={config.features?.selectable ?? false}
onCheckedChange={(checked) => updateFeatures("selectable", checked)}
/>
</div> </div>
</div> </CollapsibleContent>
</div> </Collapsible>
{/* 고급 설정 - Collapsible (소스 디테일 등) */} {/* 고급 설정 - Collapsible (소스 디테일 등) */}
<Collapsible> <Collapsible>
@ -1328,103 +1351,131 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
{/* 컬럼 설정 탭 */} {/* 컬럼 설정 탭 */}
<TabsContent value="columns" className="mt-4 space-y-4"> <TabsContent value="columns" className="mt-4 space-y-4">
{/* 통합 컬럼 선택 */} {/* 통합 컬럼 선택 - Collapsible */}
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <Collapsible open={columnSelectOpen} onOpenChange={setColumnSelectOpen}>
<div className="flex items-center gap-2"> <CollapsibleTrigger asChild>
<Database className="h-4 w-4 text-primary" /> <button
<span className="text-sm font-medium"> type="button"
{isModalMode ? "어떤 컬럼을 표시하고 입력받을까요?" : "어떤 컬럼을 입력받을까요?"} 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"
</span> >
</div> <div className="flex items-center gap-2">
<p className="text-[11px] text-muted-foreground"> <Database className="h-4 w-4 text-muted-foreground" />
{isModalMode <span className="truncate text-sm font-medium">
? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요" {isModalMode ? "컬럼 표시/입력" : "입력 컬럼"}
: "체크한 컬럼이 리피터에 입력 필드로 표시돼요" </span>
} <Badge variant="secondary" className="text-[10px] h-5">
</p> {inputableColumns.length}
</Badge>
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
{isModalMode && config.dataSource?.sourceTable && (
<>
<div className="text-[10px] font-medium text-primary mt-2 mb-1 flex items-center gap-1">
<Link2 className="h-3 w-3" />
({config.dataSource.sourceTable}) -
</div> </div>
{loadingSourceColumns ? ( <ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", columnSelectOpen && "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">
{isModalMode
? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요"
: "체크한 컬럼이 리피터에 입력 필드로 표시돼요"
}
</p>
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
{isModalMode && config.dataSource?.sourceTable && (
<>
<div className="text-[10px] font-medium text-primary mt-2 mb-1 flex items-center gap-1">
<Link2 className="h-3 w-3" />
<span className="truncate"> ({config.dataSource.sourceTable}) - </span>
</div>
{loadingSourceColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : sourceTableColumns.length === 0 ? (
<p className="text-muted-foreground py-2 text-xs"> </p>
) : (
<div className="max-h-[150px] space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
{sourceTableColumns.map((column) => (
<div
key={`source-${column.columnName}`}
className={cn(
"hover:bg-primary/10/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isSourceColumnSelected(column.columnName) && "bg-primary/10",
)}
onClick={() => toggleSourceDisplayColumn(column)}
>
<Checkbox
checked={isSourceColumnSelected(column.columnName)}
onCheckedChange={() => toggleSourceDisplayColumn(column)}
className="pointer-events-none h-3.5 w-3.5"
/>
<Link2 className="text-primary h-3 w-3 flex-shrink-0" />
<span className="truncate text-xs">{column.displayName}</span>
<span className="text-[10px] text-primary/80 ml-auto"></span>
</div>
))}
</div>
)}
</>
)}
{/* 저장 테이블 컬럼 (입력용) */}
<div className="text-[10px] font-medium text-muted-foreground mt-3 mb-1 flex items-center gap-1">
<Database className="h-3 w-3" />
<span className="truncate"> ({targetTableForColumns || "미선택"}) - </span>
</div>
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p> <p className="text-muted-foreground py-2 text-xs"> ...</p>
) : sourceTableColumns.length === 0 ? ( ) : inputableColumns.length === 0 ? (
<p className="text-muted-foreground py-2 text-xs"> </p> <p className="text-muted-foreground py-2 text-xs">
</p>
) : ( ) : (
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2"> <div className="max-h-[250px] space-y-0.5 overflow-y-auto rounded-md border p-2">
{sourceTableColumns.map((column) => ( {inputableColumns.map((column) => (
<div <div
key={`source-${column.columnName}`} key={`input-${column.columnName}`}
className={cn( className={cn(
"hover:bg-primary/10/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1", "hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isSourceColumnSelected(column.columnName) && "bg-primary/10", isColumnAdded(column.columnName) && "bg-primary/10",
)} )}
onClick={() => toggleSourceDisplayColumn(column)} onClick={() => toggleInputColumn(column)}
> >
<Checkbox <Checkbox
checked={isSourceColumnSelected(column.columnName)} checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleSourceDisplayColumn(column)} onCheckedChange={() => toggleInputColumn(column)}
className="pointer-events-none h-3.5 w-3.5" className="pointer-events-none h-3.5 w-3.5"
/> />
<Link2 className="text-primary h-3 w-3 flex-shrink-0" /> <Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<span className="truncate text-xs">{column.displayName}</span> <span className="truncate text-xs">{column.displayName}</span>
<span className="text-[10px] text-primary/80 ml-auto"></span> <span className="text-[10px] text-muted-foreground/70 ml-auto">{column.inputType}</span>
</div> </div>
))} ))}
</div> </div>
)} )}
</>
)}
{/* 저장 테이블 컬럼 (입력용) */}
<div className="text-[10px] font-medium text-muted-foreground mt-3 mb-1 flex items-center gap-1">
<Database className="h-3 w-3" />
({targetTableForColumns || "미선택"}) -
</div>
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : inputableColumns.length === 0 ? (
<p className="text-muted-foreground py-2 text-xs">
</p>
) : (
<div className="max-h-36 space-y-0.5 overflow-y-auto rounded-md border p-2">
{inputableColumns.map((column) => (
<div
key={`input-${column.columnName}`}
className={cn(
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isColumnAdded(column.columnName) && "bg-primary/10",
)}
onClick={() => toggleInputColumn(column)}
>
<Checkbox
checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleInputColumn(column)}
className="pointer-events-none h-3.5 w-3.5"
/>
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<span className="truncate text-xs">{column.displayName}</span>
<span className="text-[10px] text-muted-foreground/70 ml-auto">{column.inputType}</span>
</div>
))}
</div> </div>
)} </CollapsibleContent>
</div> </Collapsible>
{/* 선택된 컬럼 상세 설정 */} {/* 선택된 컬럼 상세 설정 - Collapsible */}
{config.columns.length > 0 && ( {config.columns.length > 0 && (
<> <Collapsible open={selectedColumnsOpen} onOpenChange={setSelectedColumnsOpen}>
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <CollapsibleTrigger asChild>
<div className="flex items-center justify-between"> <button
<span className="text-sm font-medium"> ({config.columns.length})</span> type="button"
<span className="text-[11px] text-muted-foreground"> </span> 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> >
<div className="max-h-48 space-y-1 overflow-y-auto"> <div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.columns.length}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", selectedColumnsOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
<p className="text-[10px] text-muted-foreground"> , </p>
<div className="max-h-[250px] space-y-1 overflow-y-auto">
{config.columns.map((col, index) => ( {config.columns.map((col, index) => (
<div key={col.key} className="space-y-1"> <div key={col.key} className="space-y-1">
{/* 컬럼 헤더 (드래그 가능) */} {/* 컬럼 헤더 (드래그 가능) */}
@ -1744,25 +1795,38 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
))} ))}
</div> </div>
</div> </div>
</> </CollapsibleContent>
</Collapsible>
)} )}
{/* 계산 규칙 */} {/* 계산 규칙 - Collapsible (기본 닫힘) */}
{(isModalMode || isInlineMode) && config.columns.length > 0 && ( {(isModalMode || isInlineMode) && config.columns.length > 0 && (
<> <Collapsible open={calcRulesOpen} onOpenChange={setCalcRulesOpen}>
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <CollapsibleTrigger asChild>
<div className="flex items-center justify-between"> <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"> <div className="flex items-center gap-2">
<Calculator className="h-4 w-4 text-primary" /> <Calculator className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{calculationRules.length}
</Badge>
</div> </div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", calcRulesOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
<div className="flex justify-end">
<Button type="button" variant="outline" size="sm" onClick={addCalculationRule} className="h-7 px-2 text-xs"> <Button type="button" variant="outline" size="sm" onClick={addCalculationRule} className="h-7 px-2 text-xs">
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
</Button> </Button>
</div> </div>
<div className="space-y-2"> <div className="max-h-[250px] space-y-2 overflow-y-auto">
{calculationRules.map((rule) => ( {calculationRules.map((rule) => (
<div key={rule.id} className="space-y-1 rounded border p-1.5"> <div key={rule.id} className="space-y-1 rounded border p-1.5">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -1862,7 +1926,8 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
)} )}
</div> </div>
</div> </div>
</> </CollapsibleContent>
</Collapsible>
)} )}
</TabsContent> </TabsContent>
@ -1897,94 +1962,126 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
)} )}
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-2">
{entityJoinData.joinTables.map((joinTable, tableIndex) => { {entityJoinData.joinTables.map((joinTable, tableIndex) => {
const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || ""; const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || "";
const activeCount = joinTable.availableColumns.filter(col =>
isEntityJoinColumnActive(joinTable.tableName, sourceColumn, col.columnName)
).length;
const isSubOpen = entityJoinSubOpen[tableIndex] ?? false;
return ( return (
<div key={tableIndex} className="space-y-1"> <Collapsible key={tableIndex} open={isSubOpen} onOpenChange={(open) => setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}>
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary"> <CollapsibleTrigger asChild>
<Link2 className="h-3 w-3" /> <button
<span>{joinTable.tableName}</span> type="button"
<span className="text-muted-foreground">({sourceColumn})</span> className="flex w-full items-center justify-between rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-left transition-colors hover:bg-primary/10"
</div> >
<div className="max-h-40 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2"> <div className="flex items-center gap-2">
{joinTable.availableColumns.map((column, colIndex) => { <Link2 className="h-3 w-3 text-primary" />
const isActive = isEntityJoinColumnActive( <span className="truncate text-xs font-medium">{joinTable.tableName}</span>
joinTable.tableName, <Badge variant="secondary" className="text-[10px] h-5">
sourceColumn, {activeCount > 0 ? `${activeCount}/${joinTable.availableColumns.length}개 선택` : `${joinTable.availableColumns.length}개 컬럼`}
column.columnName, </Badge>
); </div>
const matchingCol = config.columns.find((c) => c.key === column.columnName); <ChevronDown className={cn("h-3 w-3 text-muted-foreground transition-transform duration-200", isSubOpen && "rotate-180")} />
const displayField = matchingCol?.key || column.columnName; </button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[250px] space-y-0.5 overflow-y-auto rounded-b-md border border-t-0 border-primary/20 bg-primary/5 p-2">
{joinTable.availableColumns.map((column, colIndex) => {
const isActive = isEntityJoinColumnActive(
joinTable.tableName,
sourceColumn,
column.columnName,
);
const matchingCol = config.columns.find((c) => c.key === column.columnName);
const displayField = matchingCol?.key || column.columnName;
return ( return (
<div <div
key={colIndex} key={colIndex}
className={cn( className={cn(
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10/50", "flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10",
isActive && "bg-primary/10", isActive && "bg-primary/10",
)} )}
onClick={() => onClick={() =>
toggleEntityJoinColumn( toggleEntityJoinColumn(
joinTable.tableName, joinTable.tableName,
sourceColumn, sourceColumn,
column.columnName, column.columnName,
column.columnLabel, column.columnLabel,
displayField, displayField,
) )
} }
> >
<Checkbox <Checkbox
checked={isActive} checked={isActive}
className="pointer-events-none h-3.5 w-3.5" className="pointer-events-none h-3.5 w-3.5"
/> />
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" /> <Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
<span className="truncate text-xs">{column.columnLabel}</span> <span className="truncate text-xs">{column.columnLabel}</span>
<span className="ml-auto text-[10px] text-primary/80"> <span className="ml-auto text-[10px] text-primary/80">
{column.inputType || column.dataType} {column.inputType || column.dataType}
</span> </span>
</div> </div>
); );
})} })}
</div> </div>
</div> </CollapsibleContent>
</Collapsible>
); );
})} })}
</div> </div>
)} )}
{/* 현재 설정된 Entity 조인 목록 */} {/* 현재 설정된 Entity 조인 목록 - Collapsible */}
{config.entityJoins && config.entityJoins.length > 0 && ( {config.entityJoins && config.entityJoins.length > 0 && (
<div className="space-y-2 border-t pt-3"> <Collapsible open={configuredJoinsOpen} onOpenChange={setConfiguredJoinsOpen}>
<span className="text-xs font-medium"> ({config.entityJoins.length})</span> <CollapsibleTrigger asChild>
<div className="space-y-1"> <button
{config.entityJoins.map((join, idx) => ( type="button"
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]"> className="flex w-full items-center justify-between rounded-md border bg-muted/30 px-3 py-2 text-left transition-colors hover:bg-muted/50"
<Database className="h-3 w-3 text-primary" /> >
<span className="font-medium">{join.sourceColumn}</span> <div className="flex items-center gap-2">
<ArrowRight className="h-3 w-3 text-muted-foreground" /> <Database className="h-3.5 w-3.5 text-muted-foreground" />
<span>{join.referenceTable}</span> <span className="text-xs font-medium"> </span>
<span className="text-muted-foreground"> <Badge variant="secondary" className="text-[10px] h-5">
({join.columns.map((c) => c.referenceField).join(", ")}) {config.entityJoins.length}
</span> </Badge>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
updateConfig({
entityJoins: config.entityJoins!.filter((_, i) => i !== idx),
});
}}
className="ml-auto h-4 w-4 p-0 text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div> </div>
))} <ChevronDown className={cn("h-3 w-3 text-muted-foreground transition-transform duration-200", configuredJoinsOpen && "rotate-180")} />
</div> </button>
</div> </CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[250px] space-y-1 overflow-y-auto rounded-b-md border border-t-0 p-2">
{config.entityJoins.map((join, idx) => (
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]">
<Database className="h-3 w-3 text-primary flex-shrink-0" />
<span className="truncate font-medium">{join.sourceColumn}</span>
<ArrowRight className="h-3 w-3 text-muted-foreground flex-shrink-0" />
<span className="truncate">{join.referenceTable}</span>
<span className="truncate text-muted-foreground">
({join.columns.map((c) => c.referenceField).join(", ")})
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
updateConfig({
entityJoins: config.entityJoins!.filter((_, i) => i !== idx),
});
}}
className="ml-auto h-4 w-4 p-0 text-destructive flex-shrink-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
)} )}
</div> </div>
</TabsContent> </TabsContent>

View File

@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
@ -47,6 +48,9 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
const [sourceTableOpen, setSourceTableOpen] = useState(false); const [sourceTableOpen, setSourceTableOpen] = useState(false);
const [resourceTableOpen, setResourceTableOpen] = useState(false); const [resourceTableOpen, setResourceTableOpen] = useState(false);
const [customTableOpen, setCustomTableOpen] = useState(false); const [customTableOpen, setCustomTableOpen] = useState(false);
const [scheduleDataOpen, setScheduleDataOpen] = useState(true);
const [sourceDataOpen, setSourceDataOpen] = useState(true);
const [resourceOpen, setResourceOpen] = useState(true);
const [displayOpen, setDisplayOpen] = useState(false); const [displayOpen, setDisplayOpen] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false);
@ -185,18 +189,25 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* ─── 1단계: 스케줄 데이터 테이블 설정 ─── */} {/* ─── 1단계: 스케줄 데이터 테이블 설정 ─── */}
<div className="space-y-2"> <Collapsible open={scheduleDataOpen} onOpenChange={setScheduleDataOpen}>
<div className="flex items-center gap-2"> <CollapsibleTrigger asChild>
<Layers className="h-4 w-4 text-muted-foreground" /> <button
<p className="text-sm font-medium"> </p> type="button"
</div> 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"
<p className="text-[11px] text-muted-foreground"> / </p> >
</div> <div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" />
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">8 </Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", scheduleDataOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<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 className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.scheduleType || "PRODUCTION"} value={config.scheduleType || "PRODUCTION"}
onValueChange={(v) => updateConfig({ scheduleType: v as ScheduleType })} onValueChange={(v) => updateConfig({ scheduleType: v as ScheduleType })}
@ -289,7 +300,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 디자인 모드 표시용 테이블명 (selectedTable) */} {/* 디자인 모드 표시용 테이블명 (selectedTable) */}
{!config.useCustomTable && ( {!config.useCustomTable && (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Input <Input
value={config.selectedTable || "schedule_mng"} value={config.selectedTable || "schedule_mng"}
onChange={(e) => updateConfig({ selectedTable: e.target.value })} onChange={(e) => updateConfig({ selectedTable: e.target.value })}
@ -301,10 +312,10 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 스케줄 필드 매핑 */} {/* 스케줄 필드 매핑 */}
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1"> <div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
<p className="text-xs font-medium text-primary"> </p> <p className="text-xs font-medium text-primary truncate"> </p>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground">ID *</span> <span className="text-xs text-muted-foreground truncate">ID *</span>
<Select <Select
value={config.fieldMapping?.id || "schedule_id"} value={config.fieldMapping?.id || "schedule_id"}
onValueChange={(v) => updateFieldMapping("id", v)} onValueChange={(v) => updateFieldMapping("id", v)}
@ -323,7 +334,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> ID *</span> <span className="text-xs text-muted-foreground truncate"> ID *</span>
<Select <Select
value={config.fieldMapping?.resourceId || "resource_id"} value={config.fieldMapping?.resourceId || "resource_id"}
onValueChange={(v) => updateFieldMapping("resourceId", v)} onValueChange={(v) => updateFieldMapping("resourceId", v)}
@ -342,7 +353,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> *</span> <span className="text-xs text-muted-foreground truncate"> *</span>
<Select <Select
value={config.fieldMapping?.title || "schedule_name"} value={config.fieldMapping?.title || "schedule_name"}
onValueChange={(v) => updateFieldMapping("title", v)} onValueChange={(v) => updateFieldMapping("title", v)}
@ -361,7 +372,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> *</span> <span className="text-xs text-muted-foreground truncate"> *</span>
<Select <Select
value={config.fieldMapping?.startDate || "start_date"} value={config.fieldMapping?.startDate || "start_date"}
onValueChange={(v) => updateFieldMapping("startDate", v)} onValueChange={(v) => updateFieldMapping("startDate", v)}
@ -380,7 +391,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> *</span> <span className="text-xs text-muted-foreground truncate"> *</span>
<Select <Select
value={config.fieldMapping?.endDate || "end_date"} value={config.fieldMapping?.endDate || "end_date"}
onValueChange={(v) => updateFieldMapping("endDate", v)} onValueChange={(v) => updateFieldMapping("endDate", v)}
@ -399,7 +410,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.fieldMapping?.status || ""} value={config.fieldMapping?.status || ""}
onValueChange={(v) => updateFieldMapping("status", v)} onValueChange={(v) => updateFieldMapping("status", v)}
@ -418,7 +429,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.fieldMapping?.progress || ""} value={config.fieldMapping?.progress || ""}
onValueChange={(v) => updateFieldMapping("progress", v)} onValueChange={(v) => updateFieldMapping("progress", v)}
@ -437,7 +448,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.fieldMapping?.color || ""} value={config.fieldMapping?.color || ""}
onValueChange={(v) => updateFieldMapping("color", v)} onValueChange={(v) => updateFieldMapping("color", v)}
@ -454,22 +465,33 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
</div> </div>
</div> </CollapsibleContent>
</Collapsible>
{/* ─── 2단계: 소스 데이터 설정 ─── */} {/* ─── 2단계: 소스 데이터 설정 ─── */}
<div className="space-y-2"> <Collapsible open={sourceDataOpen} onOpenChange={setSourceDataOpen}>
<div className="flex items-center gap-2"> <CollapsibleTrigger asChild>
<Database className="h-4 w-4 text-muted-foreground" /> <button
<p className="text-sm font-medium"> </p> type="button"
</div> 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"
<p className="text-[11px] text-muted-foreground"> </p> >
</div> <div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.sourceConfig?.tableName ? "4개 필드" : "미설정"}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", sourceDataOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
{/* 소스 테이블 Combobox */} {/* 소스 테이블 Combobox */}
<div className="space-y-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> (/ )</span> <span className="text-xs text-muted-foreground truncate"> (/ )</span>
<Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}> <Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -528,11 +550,11 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 소스 필드 매핑 (테이블 선택 시) */} {/* 소스 필드 매핑 (테이블 선택 시) */}
{config.sourceConfig?.tableName && ( {config.sourceConfig?.tableName && (
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1"> <div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
<p className="text-xs font-medium text-primary"> </p> <p className="text-xs font-medium text-primary truncate"> </p>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<span className="text-xs text-muted-foreground"> (/) *</span> <span className="text-xs text-muted-foreground truncate"> (/) *</span>
<p className="text-[10px] text-muted-foreground mt-0.5"> </p> <p className="text-[10px] text-muted-foreground mt-0.5"> </p>
</div> </div>
<Select <Select
@ -553,7 +575,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.sourceConfig?.quantityField || ""} value={config.sourceConfig?.quantityField || ""}
onValueChange={(v) => updateSourceConfig({ quantityField: v })} onValueChange={(v) => updateSourceConfig({ quantityField: v })}
@ -572,7 +594,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> ()</span> <span className="text-xs text-muted-foreground truncate"> ()</span>
<Select <Select
value={config.sourceConfig?.groupByField || ""} value={config.sourceConfig?.groupByField || ""}
onValueChange={(v) => updateSourceConfig({ groupByField: v })} onValueChange={(v) => updateSourceConfig({ groupByField: v })}
@ -591,7 +613,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> ()</span> <span className="text-xs text-muted-foreground truncate"> ()</span>
<Select <Select
value={config.sourceConfig?.groupNameField || ""} value={config.sourceConfig?.groupNameField || ""}
onValueChange={(v) => updateSourceConfig({ groupNameField: v })} onValueChange={(v) => updateSourceConfig({ groupNameField: v })}
@ -610,21 +632,32 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
</div> </div>
)} )}
</div> </div>
</CollapsibleContent>
</Collapsible>
{/* ─── 3단계: 리소스 설정 ─── */} {/* ─── 3단계: 리소스 설정 ─── */}
<div className="space-y-2"> <Collapsible open={resourceOpen} onOpenChange={setResourceOpen}>
<div className="flex items-center gap-2"> <CollapsibleTrigger asChild>
<Users className="h-4 w-4 text-muted-foreground" /> <button
<p className="text-sm font-medium"> (/)</p> type="button"
</div> 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"
<p className="text-[11px] text-muted-foreground"> Y축에 </p> >
</div> <div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" />
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> <span className="text-sm font-medium"> (/)</span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.resourceTable ? "3개 필드" : "미설정"}
</Badge>
</div>
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", resourceOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
{/* 리소스 테이블 Combobox */} {/* 리소스 테이블 Combobox */}
<div className="space-y-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Popover open={resourceTableOpen} onOpenChange={setResourceTableOpen}> <Popover open={resourceTableOpen} onOpenChange={setResourceTableOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -682,10 +715,10 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 리소스 필드 매핑 */} {/* 리소스 필드 매핑 */}
{config.resourceTable && ( {config.resourceTable && (
<div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1"> <div className="ml-1 border-l-2 border-primary/20 pl-3 space-y-2 pt-1">
<p className="text-xs font-medium text-primary"> </p> <p className="text-xs font-medium text-primary truncate"> </p>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground">ID </span> <span className="text-xs text-muted-foreground truncate">ID </span>
<Select <Select
value={config.resourceFieldMapping?.id || ""} value={config.resourceFieldMapping?.id || ""}
onValueChange={(v) => updateResourceFieldMapping("id", v)} onValueChange={(v) => updateResourceFieldMapping("id", v)}
@ -704,7 +737,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.resourceFieldMapping?.name || ""} value={config.resourceFieldMapping?.name || ""}
onValueChange={(v) => updateResourceFieldMapping("name", v)} onValueChange={(v) => updateResourceFieldMapping("name", v)}
@ -723,7 +756,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.resourceFieldMapping?.group || ""} value={config.resourceFieldMapping?.group || ""}
onValueChange={(v) => updateResourceFieldMapping("group", v)} onValueChange={(v) => updateResourceFieldMapping("group", v)}
@ -742,7 +775,9 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</div> </div>
</div> </div>
)} )}
</div> </div>
</CollapsibleContent>
</Collapsible>
{/* ─── 4단계: 표시 설정 (Collapsible) ─── */} {/* ─── 4단계: 표시 설정 (Collapsible) ─── */}
<Collapsible open={displayOpen} onOpenChange={setDisplayOpen}> <Collapsible open={displayOpen} onOpenChange={setDisplayOpen}>
@ -754,6 +789,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" /> <Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">18</Badge>
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
@ -764,10 +800,10 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <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 className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<Select <Select
value={config.defaultZoomLevel || "day"} value={config.defaultZoomLevel || "day"}
onValueChange={(v) => updateConfig({ defaultZoomLevel: v as ZoomLevel })} onValueChange={(v) => updateConfig({ defaultZoomLevel: v as ZoomLevel })}
@ -788,7 +824,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 초기 표시 날짜 */} {/* 초기 표시 날짜 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground truncate"> </span>
<p className="text-[10px] text-muted-foreground mt-0.5"> </p> <p className="text-[10px] text-muted-foreground mt-0.5"> </p>
</div> </div>
<Input <Input
@ -801,7 +837,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 높이 */} {/* 높이 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> (px)</span> <span className="text-xs text-muted-foreground truncate"> (px)</span>
<Input <Input
type="number" type="number"
value={config.height || 500} value={config.height || 500}
@ -812,7 +848,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 최대 높이 */} {/* 최대 높이 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> (px)</span> <span className="text-xs text-muted-foreground truncate"> (px)</span>
<Input <Input
type="number" type="number"
value={typeof config.maxHeight === "number" ? config.maxHeight : ""} value={typeof config.maxHeight === "number" ? config.maxHeight : ""}
@ -827,7 +863,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 행 높이 */} {/* 행 높이 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> (px)</span> <span className="text-xs text-muted-foreground truncate"> (px)</span>
<Input <Input
type="number" type="number"
value={config.rowHeight || 50} value={config.rowHeight || 50}
@ -838,7 +874,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 헤더 높이 */} {/* 헤더 높이 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> (px)</span> <span className="text-xs text-muted-foreground truncate"> (px)</span>
<Input <Input
type="number" type="number"
value={config.headerHeight || 60} value={config.headerHeight || 60}
@ -849,7 +885,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 리소스 컬럼 너비 */} {/* 리소스 컬럼 너비 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> (px)</span> <span className="text-xs text-muted-foreground truncate"> (px)</span>
<Input <Input
type="number" type="number"
value={config.resourceColumnWidth || 150} value={config.resourceColumnWidth || 150}
@ -860,7 +896,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
{/* 셀 너비 (줌 레벨별) */} {/* 셀 너비 (줌 레벨별) */}
<div className="space-y-2 pt-1"> <div className="space-y-2 pt-1">
<span className="text-xs text-muted-foreground"> ( , px)</span> <span className="text-xs text-muted-foreground truncate"> ( , px)</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1"> <div className="flex-1">
<span className="text-[10px] text-muted-foreground"></span> <span className="text-[10px] text-muted-foreground"></span>
@ -1016,6 +1052,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" /> <Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">5</Badge>
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
@ -1026,7 +1063,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <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">
{[ {[
{ key: "planned", label: "계획됨", defaultColor: "#3b82f6" }, { key: "planned", label: "계획됨", defaultColor: "#3b82f6" },
{ key: "in_progress", label: "진행중", defaultColor: "#f59e0b" }, { key: "in_progress", label: "진행중", defaultColor: "#f59e0b" },