[agent-pipeline] pipe-20260311225813-8hmk round-1
This commit is contained in:
parent
db3ad9d639
commit
bb442f5478
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue