[agent-pipeline] pipe-20260311221723-l7a9 round-1

This commit is contained in:
DDD1542 2026-03-12 07:35:09 +09:00
parent 5093863e08
commit 8ad0c8797d
4 changed files with 1437 additions and 1204 deletions

View File

@ -13,8 +13,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
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";
import { Badge } from "@/components/ui/badge";
import { import {
Settings, ChevronDown, Plus, Trash2, Check, ChevronsUpDown, Settings, ChevronDown, ChevronRight, Plus, Trash2, Check, ChevronsUpDown,
Database, Monitor, Columns, Database, Monitor, Columns,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -144,10 +145,12 @@ function ColumnCombobox({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className="h-7 w-[140px] justify-between text-xs" className="h-7 w-full justify-between text-xs"
disabled={loading || !tableName} disabled={loading || !tableName}
> >
<span className="truncate">
{loading ? "로딩..." : !tableName ? "테이블 먼저 선택" : selected ? selected.displayName || selected.columnName : placeholder || "컬럼 선택"} {loading ? "로딩..." : !tableName ? "테이블 먼저 선택" : selected ? selected.displayName || selected.columnName : placeholder || "컬럼 선택"}
</span>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -220,10 +223,12 @@ function ScreenCombobox({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className="h-7 w-[140px] justify-between text-xs" className="h-7 w-full justify-between text-xs"
disabled={loading} disabled={loading}
> >
<span className="truncate">
{loading ? "로딩..." : selected ? selected.screenName : "화면 선택"} {loading ? "로딩..." : selected ? selected.screenName : "화면 선택"}
</span>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -262,6 +267,8 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
}) => { }) => {
const [tables, setTables] = useState<TableInfo[]>([]); const [tables, setTables] = useState<TableInfo[]>([]);
const [loadingTables, setLoadingTables] = useState(false); const [loadingTables, setLoadingTables] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [columnsOpen, setColumnsOpen] = useState(false);
const [dataSourceOpen, setDataSourceOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false);
const [layoutOpen, setLayoutOpen] = useState(false); const [layoutOpen, setLayoutOpen] = useState(false);
@ -344,78 +351,145 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* ─── 1단계: 모달 연동 ─── */} {/* ─── 1단계: 모달 연동 (Collapsible) ─── */}
<div className="space-y-2"> <Collapsible open={modalOpen} onOpenChange={setModalOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Monitor className="h-4 w-4 text-muted-foreground" /> <Monitor className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium"> </p> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{[config.modals.versionAddScreenId, config.modals.processAddScreenId, config.modals.processEditScreenId].filter(Boolean).length}
</Badge>
</div> </div>
<p className="text-[11px] text-muted-foreground"> / · </p> <ChevronDown
</div> className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
<div className="rounded-lg border bg-muted/30 p-4 space-y-3"> modalOpen && "rotate-180",
<div className="flex items-center justify-between py-1"> )}
<span className="text-xs text-muted-foreground"> </span> />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-2">
<p className="text-[10px] text-muted-foreground"> / · </p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox <ScreenCombobox
value={config.modals.versionAddScreenId} value={config.modals.versionAddScreenId}
onChange={(v) => updateModals("versionAddScreenId", v)} onChange={(v) => updateModals("versionAddScreenId", v)}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox <ScreenCombobox
value={config.modals.processAddScreenId} value={config.modals.processAddScreenId}
onChange={(v) => updateModals("processAddScreenId", v)} onChange={(v) => updateModals("processAddScreenId", v)}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-[10px] text-muted-foreground"> </span>
<ScreenCombobox <ScreenCombobox
value={config.modals.processEditScreenId} value={config.modals.processEditScreenId}
onChange={(v) => updateModals("processEditScreenId", v)} onChange={(v) => updateModals("processEditScreenId", v)}
/> />
</div> </div>
</div> </div>
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 2단계: 공정 테이블 컬럼 ─── */} {/* ─── 2단계: 공정 테이블 컬럼 (Collapsible + 접이식 카드) ─── */}
<div className="space-y-2"> <Collapsible open={columnsOpen} onOpenChange={setColumnsOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Columns className="h-4 w-4 text-muted-foreground" /> <Columns className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium"> </p> <span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.processColumns.length}
</Badge>
</div> </div>
<p className="text-[11px] text-muted-foreground"> </p> <ChevronDown
</div> className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
<div className="rounded-lg border bg-muted/30 p-4 space-y-2"> columnsOpen && "rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
<p className="text-[10px] text-muted-foreground mb-1"> </p>
<div className="max-h-[250px] space-y-1 overflow-y-auto">
{config.processColumns.map((col, idx) => ( {config.processColumns.map((col, idx) => (
<div <Collapsible key={idx}>
key={idx} <div className="rounded-md border">
className="flex items-center gap-1.5 rounded-md border bg-background p-2" <CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left hover:bg-muted/30 transition-colors"
> >
<ChevronRight className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
<span className="text-xs font-medium truncate flex-1 min-w-0">{col.name || "미설정"}</span>
<span className="text-[10px] text-muted-foreground truncate max-w-[60px] shrink-0">{col.label}</span>
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{col.align || "left"}</Badge>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => { e.stopPropagation(); removeColumn(idx); }}
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-2 gap-1.5 border-t px-2.5 py-2">
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input <Input
value={col.name} value={col.name}
onChange={(e) => updateColumn(idx, "name", e.target.value)} onChange={(e) => updateColumn(idx, "name", e.target.value)}
className="h-7 w-24 text-[10px]" className="h-7 text-xs"
placeholder="컬럼명" placeholder="컬럼명"
/> />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input <Input
value={col.label} value={col.label}
onChange={(e) => updateColumn(idx, "label", e.target.value)} onChange={(e) => updateColumn(idx, "label", e.target.value)}
className="h-7 flex-1 text-[10px]" className="h-7 text-xs"
placeholder="표시명" placeholder="표시명"
/> />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input <Input
type="number" type="number"
value={col.width || 100} value={col.width || 100}
onChange={(e) => updateColumn(idx, "width", parseInt(e.target.value) || 100)} onChange={(e) => updateColumn(idx, "width", parseInt(e.target.value) || 100)}
className="h-7 w-14 text-[10px]" className="h-7 text-xs"
placeholder="너비" placeholder="100"
/> />
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Select <Select
value={col.align || "left"} value={col.align || "left"}
onValueChange={(v) => updateColumn(idx, "align", v)} onValueChange={(v) => updateColumn(idx, "align", v)}
> >
<SelectTrigger className="h-7 w-16 text-[10px]"> <SelectTrigger className="h-7 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -424,27 +498,25 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
<SelectItem value="right"></SelectItem> <SelectItem value="right"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 text-destructive hover:text-destructive"
onClick={() => removeColumn(idx)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div> </div>
</div>
</CollapsibleContent>
</div>
</Collapsible>
))} ))}
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-7 w-full gap-1 text-xs" className="h-7 w-full gap-1 text-xs border-dashed"
onClick={addColumn} onClick={addColumn}
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
</div> </div>
</CollapsibleContent>
</Collapsible>
{/* ─── 3단계: 데이터 소스 (Collapsible) ─── */} {/* ─── 3단계: 데이터 소스 (Collapsible) ─── */}
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}> <Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
@ -456,6 +528,11 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" /> <Database className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
{config.dataSource.itemTable && (
<Badge variant="secondary" className="text-[10px] h-5 truncate max-w-[100px]">
{config.dataSource.itemTable}
</Badge>
)}
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
@ -476,7 +553,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
loading={loadingTables} loading={loadingTables}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.itemNameColumn} value={config.dataSource.itemNameColumn}
@ -485,7 +562,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
placeholder="품목명" placeholder="품목명"
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.itemCodeColumn} value={config.dataSource.itemCodeColumn}
@ -503,7 +580,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
loading={loadingTables} loading={loadingTables}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> FK </span> <span className="text-xs text-muted-foreground"> FK </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.routingVersionFkColumn} value={config.dataSource.routingVersionFkColumn}
@ -512,7 +589,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
placeholder="FK 컬럼" placeholder="FK 컬럼"
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.routingVersionNameColumn} value={config.dataSource.routingVersionNameColumn}
@ -530,7 +607,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
loading={loadingTables} loading={loadingTables}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> FK </span> <span className="text-xs text-muted-foreground"> FK </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.routingDetailFkColumn} value={config.dataSource.routingDetailFkColumn}
@ -548,7 +625,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
loading={loadingTables} loading={loadingTables}
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.processNameColumn} value={config.dataSource.processNameColumn}
@ -557,7 +634,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
placeholder="공정명" placeholder="공정명"
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span> <span className="text-xs text-muted-foreground"> </span>
<ColumnCombobox <ColumnCombobox
value={config.dataSource.processCodeColumn} value={config.dataSource.processCodeColumn}

View File

@ -2,7 +2,7 @@
/** /**
* V2 * V2
* UX: 데이터 -> -> -> () * Progressive Disclosure: 작업 -> -> ()
*/ */
import React, { useState } from "react"; import React, { useState } from "react";
@ -10,7 +10,8 @@ import { Input } from "@/components/ui/input";
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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Settings, ChevronDown, Plus, Trash2, GripVertical, Database, Layers } from "lucide-react"; import { Badge } from "@/components/ui/badge";
import { Settings, ChevronDown, ChevronRight, Plus, Trash2, Database, Layers, List } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { import type {
ProcessWorkStandardConfig, ProcessWorkStandardConfig,
@ -28,8 +29,10 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
config: configProp, config: configProp,
onChange, onChange,
}) => { }) => {
const [phasesOpen, setPhasesOpen] = useState(false);
const [detailTypesOpen, setDetailTypesOpen] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [dataSourceOpen, setDataSourceOpen] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false);
const [layoutOpen, setLayoutOpen] = useState(false);
const config: ProcessWorkStandardConfig = { const config: ProcessWorkStandardConfig = {
...defaultConfig, ...defaultConfig,
@ -90,220 +93,197 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* ─── 1단계: 작업 단계 설정 ─── */} {/* ─── 1단계: 작업 단계 설정 (Collapsible + 접이식 카드) ─── */}
<div className="space-y-2"> <Collapsible open={phasesOpen} onOpenChange={setPhasesOpen}>
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium"> </p>
</div>
<p className="text-[11px] text-muted-foreground"> (Phase) </p>
</div>
<div className="rounded-lg border bg-muted/30 p-4 space-y-2">
{config.phases.map((phase, idx) => (
<div
key={idx}
className="flex items-center gap-1.5 rounded-md border bg-background p-2"
>
<GripVertical className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
<Input
value={phase.key}
onChange={(e) => updatePhase(idx, "key", e.target.value)}
className="h-7 w-20 text-[10px]"
placeholder="키"
/>
<Input
value={phase.label}
onChange={(e) => updatePhase(idx, "label", e.target.value)}
className="h-7 flex-1 text-[10px]"
placeholder="표시명"
/>
<Input
type="number"
min={1}
value={phase.sortOrder}
onChange={(e) => updatePhase(idx, "sortOrder", parseInt(e.target.value) || 1)}
className="h-7 w-12 text-[10px] text-center"
placeholder="순서"
title="정렬 순서"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 text-destructive hover:text-destructive"
onClick={() => removePhase(idx)}
disabled={config.phases.length <= 1}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
className="h-7 w-full gap-1 text-xs"
onClick={addPhase}
>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* ─── 2단계: 상세 유형 옵션 ─── */}
<div className="space-y-2">
<p className="text-sm font-medium"> </p>
<p className="text-[11px] text-muted-foreground"> </p>
</div>
<div className="rounded-lg border bg-muted/30 p-4 space-y-2">
{config.detailTypes.map((dt, idx) => (
<div
key={idx}
className="flex items-center gap-1.5 rounded-md border bg-background p-2"
>
<Input
value={dt.value}
onChange={(e) => updateDetailType(idx, "value", e.target.value)}
className="h-7 w-24 text-[10px]"
placeholder="값"
/>
<Input
value={dt.label}
onChange={(e) => updateDetailType(idx, "label", e.target.value)}
className="h-7 flex-1 text-[10px]"
placeholder="표시명"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 text-destructive hover:text-destructive"
onClick={() => removeDetailType(idx)}
disabled={config.detailTypes.length <= 1}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
className="h-7 w-full gap-1 text-xs"
onClick={addDetailType}
>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* ─── 3단계: 데이터 소스 (Collapsible) ─── */}
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
type="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" 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">
<Database className="h-4 w-4 text-muted-foreground" /> <Layers 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">
{config.phases.length}
</Badge>
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200", "h-4 w-4 text-muted-foreground transition-transform duration-200",
dataSourceOpen && "rotate-180" phasesOpen && "rotate-180",
)} )}
/> />
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3"> <div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
<div className="flex items-center justify-between py-1"> <p className="text-[10px] text-muted-foreground mb-1"> (Phase) </p>
<span className="text-xs text-muted-foreground"> </span> <div className="max-h-[250px] space-y-1 overflow-y-auto">
{config.phases.map((phase, idx) => (
<Collapsible key={idx}>
<div className="rounded-md border">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left hover:bg-muted/30 transition-colors"
>
<ChevronRight className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
<span className="text-xs font-medium truncate flex-1 min-w-0">{phase.label}</span>
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{phase.key}</Badge>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => { e.stopPropagation(); removePhase(idx); }}
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
disabled={config.phases.length <= 1}
>
<Trash2 className="h-3 w-3" />
</Button>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-3 gap-1.5 border-t px-2.5 py-2">
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input <Input
value={config.dataSource.itemTable} value={phase.key}
onChange={(e) => updateDataSource("itemTable", e.target.value)} onChange={(e) => updatePhase(idx, "key", e.target.value)}
className="h-7 w-[160px] text-xs" className="h-7 text-xs"
placeholder="키"
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-0.5">
<span className="text-xs text-muted-foreground"> </span> <span className="text-[10px] text-muted-foreground"></span>
<Input <Input
value={config.dataSource.itemNameColumn} value={phase.label}
onChange={(e) => updateDataSource("itemNameColumn", e.target.value)} onChange={(e) => updatePhase(idx, "label", e.target.value)}
className="h-7 w-[160px] text-xs" className="h-7 text-xs"
placeholder="표시명"
/> />
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="space-y-0.5">
<span className="text-xs text-muted-foreground"> </span> <span className="text-[10px] text-muted-foreground"></span>
<Input <Input
value={config.dataSource.itemCodeColumn} type="number"
onChange={(e) => updateDataSource("itemCodeColumn", e.target.value)} min={1}
className="h-7 w-[160px] text-xs" value={phase.sortOrder}
/> onChange={(e) => updatePhase(idx, "sortOrder", parseInt(e.target.value) || 1)}
</div> className="h-7 text-xs text-center"
<div className="flex items-center justify-between py-1"> placeholder="1"
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.routingVersionTable}
onChange={(e) => updateDataSource("routingVersionTable", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> FK</span>
<Input
value={config.dataSource.routingFkColumn}
onChange={(e) => updateDataSource("routingFkColumn", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.routingVersionNameColumn}
onChange={(e) => updateDataSource("routingVersionNameColumn", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.routingDetailTable}
onChange={(e) => updateDataSource("routingDetailTable", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.processTable}
onChange={(e) => updateDataSource("processTable", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.processNameColumn}
onChange={(e) => updateDataSource("processNameColumn", e.target.value)}
className="h-7 w-[160px] text-xs"
/>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-muted-foreground"> </span>
<Input
value={config.dataSource.processCodeColumn}
onChange={(e) => updateDataSource("processCodeColumn", e.target.value)}
className="h-7 w-[160px] text-xs"
/> />
</div> </div>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</div>
</Collapsible>
))}
</div>
<Button
variant="outline"
size="sm"
className="h-7 w-full gap-1 text-xs border-dashed"
onClick={addPhase}
>
<Plus className="h-3 w-3" />
</Button>
</div>
</CollapsibleContent>
</Collapsible> </Collapsible>
{/* ─── 4단계: 레이아웃 & 기타 (Collapsible) ─── */} {/* ─── 2단계: 상세 유형 옵션 (Collapsible + 접이식 카드) ─── */}
<Collapsible open={layoutOpen} onOpenChange={setLayoutOpen}> <Collapsible open={detailTypesOpen} onOpenChange={setDetailTypesOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<List className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{config.detailTypes.length}
</Badge>
</div>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
detailTypesOpen && "rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
<p className="text-[10px] text-muted-foreground mb-1"> </p>
<div className="max-h-[250px] space-y-1 overflow-y-auto">
{config.detailTypes.map((dt, idx) => (
<Collapsible key={idx}>
<div className="rounded-md border">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left hover:bg-muted/30 transition-colors"
>
<ChevronRight className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
<span className="text-[10px] text-muted-foreground font-medium shrink-0">#{idx + 1}</span>
<span className="text-xs font-medium truncate flex-1 min-w-0">{dt.label}</span>
<Badge variant="outline" className="text-[9px] h-4 shrink-0">{dt.value}</Badge>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => { e.stopPropagation(); removeDetailType(idx); }}
className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
disabled={config.detailTypes.length <= 1}
>
<Trash2 className="h-3 w-3" />
</Button>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-2 gap-1.5 border-t px-2.5 py-2">
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input
value={dt.value}
onChange={(e) => updateDetailType(idx, "value", e.target.value)}
className="h-7 text-xs"
placeholder="값"
/>
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-muted-foreground"></span>
<Input
value={dt.label}
onChange={(e) => updateDetailType(idx, "label", e.target.value)}
className="h-7 text-xs"
placeholder="표시명"
/>
</div>
</div>
</CollapsibleContent>
</div>
</Collapsible>
))}
</div>
<Button
variant="outline"
size="sm"
className="h-7 w-full gap-1 text-xs border-dashed"
onClick={addDetailType}
>
<Plus className="h-3 w-3" />
</Button>
</div>
</CollapsibleContent>
</Collapsible>
{/* ─── 3단계: 고급 설정 (데이터 소스 + 레이아웃 통합) ─── */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
type="button" type="button"
@ -311,18 +291,21 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
> >
<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>
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200", "h-4 w-4 text-muted-foreground transition-transform duration-200",
layoutOpen && "rotate-180" advancedOpen && "rotate-180",
)} )}
/> />
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3"> <div className="rounded-b-lg border border-t-0 p-3 space-y-3">
{/* 레이아웃 기본 설정 */}
<div className="space-y-2">
<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"> (%)</span>
@ -337,21 +320,19 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
className="h-7 w-[80px] text-xs" className="h-7 w-[80px] text-xs"
/> />
</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"> </span>
<Input <Input
value={config.leftPanelTitle || ""} value={config.leftPanelTitle || ""}
onChange={(e) => update({ leftPanelTitle: e.target.value })} onChange={(e) => update({ leftPanelTitle: e.target.value })}
placeholder="품목 및 공정 선택" placeholder="품목 및 공정 선택"
className="h-7 w-[160px] text-xs" className="h-7 w-[140px] text-xs"
/> />
</div> </div>
<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-xs"> </p>
<p className="text-[11px] text-muted-foreground">/ </p> <p className="text-[10px] text-muted-foreground">/ </p>
</div> </div>
<Switch <Switch
checked={config.readonly || false} checked={config.readonly || false}
@ -359,6 +340,122 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
/> />
</div> </div>
</div> </div>
{/* 데이터 소스 (서브 Collapsible) */}
<Collapsible open={dataSourceOpen} onOpenChange={setDataSourceOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-md border px-3 py-2 transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-2">
<Database className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span>
{config.dataSource.itemTable && (
<Badge variant="secondary" className="text-[10px] h-5 truncate max-w-[100px]">
{config.dataSource.itemTable}
</Badge>
)}
</div>
<ChevronDown
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
dataSourceOpen && "rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 pt-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.itemTable}
onChange={(e) => updateDataSource("itemTable", e.target.value)}
className="h-7 w-full text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.itemNameColumn}
onChange={(e) => updateDataSource("itemNameColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.itemCodeColumn}
onChange={(e) => updateDataSource("itemCodeColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
</div>
<div className="space-y-1 pt-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.routingVersionTable}
onChange={(e) => updateDataSource("routingVersionTable", e.target.value)}
className="h-7 w-full text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> FK</span>
<Input
value={config.dataSource.routingFkColumn}
onChange={(e) => updateDataSource("routingFkColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.routingVersionNameColumn}
onChange={(e) => updateDataSource("routingVersionNameColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
</div>
<div className="space-y-1 pt-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.routingDetailTable}
onChange={(e) => updateDataSource("routingDetailTable", e.target.value)}
className="h-7 w-full text-xs"
/>
</div>
<div className="space-y-1 pt-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.processTable}
onChange={(e) => updateDataSource("processTable", e.target.value)}
className="h-7 w-full text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.processNameColumn}
onChange={(e) => updateDataSource("processNameColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<span className="text-[10px] text-muted-foreground"> </span>
<Input
value={config.dataSource.processCodeColumn}
onChange={(e) => updateDataSource("processCodeColumn", e.target.value)}
className="h-7 text-xs"
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>

View File

@ -36,6 +36,7 @@ import {
CommandList, CommandList,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { import {
Database, Database,
Table2, Table2,
@ -44,6 +45,7 @@ import {
Trash2, Trash2,
Settings, Settings,
ChevronDown, ChevronDown,
ChevronRight,
Check, Check,
ChevronsUpDown, ChevronsUpDown,
Link2, Link2,
@ -793,6 +795,11 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Table2 className="h-3.5 w-3.5 text-primary" /> <Table2 className="h-3.5 w-3.5 text-primary" />
<span className="text-xs font-semibold"> ( )</span> <span className="text-xs font-semibold"> ( )</span>
{displayColumns.length > 0 && (
<Badge variant="secondary" className="text-[10px] h-5">
{displayColumns.length}
</Badge>
)}
</div> </div>
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
@ -800,24 +807,22 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</div> </div>
{displayColumns.length > 0 && ( {displayColumns.length > 0 && (
<div className="space-y-1.5"> <div className="max-h-[180px] space-y-0.5 overflow-y-auto">
{displayColumns.map((col) => ( {displayColumns.map((col) => (
<div <div
key={col.name} key={col.name}
className="flex items-center justify-between rounded-md border px-3 py-1.5" className="flex items-center gap-2 rounded-md border px-3 py-1"
> >
<div className="flex items-center gap-2"> <span className="text-xs font-medium truncate flex-1">{col.label}</span>
<span className="text-xs font-medium">{col.label}</span> <Badge variant="outline" className="text-[9px] h-4 shrink-0">
<span className="text-[10px] text-muted-foreground">
{col.name} {col.name}
</span> </Badge>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => removeDisplayColumn(col.name)} onClick={() => removeDisplayColumn(col.name)}
className="h-6 w-6 p-0 text-destructive hover:bg-destructive/10" className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
@ -890,41 +895,71 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
<Separator /> <Separator />
{/* ════════ 4단계: 추가 입력 필드 ════════ */} {/* ════════ 4단계: 추가 입력 필드 (Collapsible) ════════ */}
<div className="space-y-1.5"> <Collapsible
<div className="flex items-center gap-1.5"> open={openSections["inputFields"] ?? (localFields.length > 0)}
<Columns3 className="h-3.5 w-3.5 text-primary" /> onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, inputFields: open }))}
<span className="text-xs font-semibold"> </span> >
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Columns3 className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
<Badge variant="secondary" className="text-[10px] h-5">
{localFields.length}
</Badge>
</div> </div>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
(openSections["inputFields"] ?? (localFields.length > 0)) && "rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-2">
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
</p> </p>
</div>
{localFields.length > 0 && ( {localFields.length > 0 && (
<div className="space-y-2"> <div className="max-h-[300px] space-y-1 overflow-y-auto">
{localFields.map((field, index) => ( {localFields.map((field, index) => (
<div <Collapsible key={index}>
key={index} <div className="rounded-md border">
className="space-y-2 rounded-lg border p-3" <CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left hover:bg-muted/30 transition-colors"
> >
<div className="flex items-center justify-between"> <ChevronRight className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90 shrink-0" />
<span className="text-xs font-medium"> <span className="text-xs font-medium flex-1 truncate min-w-0">
{index + 1}: {field.label || field.name} {field.label || field.name}
</span> </span>
<Badge variant="outline" className="text-[9px] h-4 shrink-0">
{field.inputType || field.type || "text"}
</Badge>
{field.required && (
<span className="text-[10px] text-destructive font-bold shrink-0">*</span>
)}
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => removeField(index)} onClick={(e) => { e.stopPropagation(); removeField(index); }}
className="h-5 w-5 p-0 text-destructive" className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive shrink-0"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-2 border-t px-2.5 py-2">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{/* 필드명 (컬럼 선택) */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> ()</Label> <Label className="text-[10px]"> ()</Label>
<ColumnCombobox <ColumnCombobox
@ -941,8 +976,6 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
} }
/> />
</div> </div>
{/* 라벨 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"></Label> <Label className="text-[10px]"></Label>
<Input <Input
@ -956,19 +989,6 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2">
{/* 타입 (자동) */}
<div className="space-y-1">
<Label className="text-[10px]"> ()</Label>
<Input
value={field.inputType || field.type || "text"}
readOnly
disabled
className="h-7 bg-muted text-xs"
/>
</div>
{/* Placeholder */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]">Placeholder</Label> <Label className="text-[10px]">Placeholder</Label>
<Input <Input
@ -980,9 +1000,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
className="h-7 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div>
{/* 필드 그룹 선택 */}
{localFieldGroups.length > 0 && ( {localFieldGroups.length > 0 && (
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
@ -1011,8 +1029,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</div> </div>
)} )}
{/* 필수 / 자동 채우기 */} <div className="flex items-center justify-between pt-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
id={`required-${index}`} id={`required-${index}`}
@ -1029,12 +1046,15 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</Label> </Label>
</div> </div>
{field.autoFillFrom && ( {field.autoFillFrom && (
<span className="text-[9px] text-primary"> <span className="text-[9px] text-primary truncate max-w-[120px]">
: {field.autoFillFrom} : {field.autoFillFrom}
</span> </span>
)} )}
</div> </div>
</div> </div>
</CollapsibleContent>
</div>
</Collapsible>
))} ))}
</div> </div>
)} )}
@ -1049,24 +1069,108 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />
</Button> </Button>
</div>
</CollapsibleContent>
</Collapsible>
{/* ════════ 5단계: 고급 설정 (서브 Collapsible 통합) ════════ */}
<Collapsible
open={openSections["advanced"] ?? false}
onOpenChange={() => toggleSection("advanced")}
>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
</div>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
openSections["advanced"] && "rotate-180",
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
{/* ─── 기본 고급 설정 ─── */}
<div className="space-y-2">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<ColumnCombobox
value={config.sourceKeyField || ""}
columns={targetColumns}
placeholder="자동 감지 (entity FK)"
onSelect={(name) => handleChange("sourceKeyField", name)}
/>
<p className="text-[10px] text-muted-foreground">
FK
</p>
</div>
<div className="flex items-center justify-between py-1">
<Label className="text-xs"> </Label>
<Switch
checked={config.showIndex ?? true}
onCheckedChange={(v) => handleChange("showIndex", v)}
/>
</div>
<div className="flex items-center justify-between py-1">
<Label className="text-xs"> </Label>
<Switch
checked={config.allowRemove ?? false}
onCheckedChange={(v) => handleChange("allowRemove", v)}
/>
</div>
<div className="flex items-center justify-between py-1">
<Label className="text-xs"></Label>
<Switch
checked={config.disabled ?? false}
onCheckedChange={(v) => handleChange("disabled", v)}
/>
</div>
<div className="flex items-center justify-between py-1">
<Label className="text-xs"> </Label>
<Switch
checked={config.readonly ?? false}
onCheckedChange={(v) => handleChange("readonly", v)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={config.emptyMessage || ""}
onChange={(e) => handleChange("emptyMessage", e.target.value)}
placeholder="전달받은 데이터가 없습니다."
className="h-8 text-xs"
/>
</div>
</div>
<Separator /> <Separator />
{/* ════════ 접히는 섹션들 ════════ */} {/* ─── 필드 그룹 관리 (서브 Collapsible) ─── */}
{/* ─── 필드 그룹 관리 (Collapsible) ─── */}
<Collapsible <Collapsible
open={openSections["fieldGroups"] ?? false} open={openSections["fieldGroups"] ?? false}
onOpenChange={() => toggleSection("fieldGroups")} onOpenChange={() => toggleSection("fieldGroups")}
> >
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border px-3 py-2 transition-colors hover:bg-muted/50"> <CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-md border px-3 py-2 transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FolderPlus className="h-3.5 w-3.5 text-muted-foreground" /> <FolderPlus className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
{localFieldGroups.length > 0 && ( {localFieldGroups.length > 0 && (
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary"> <Badge variant="secondary" className="text-[10px] h-5">
{localFieldGroups.length} {localFieldGroups.length}
</span> </Badge>
)} )}
</div> </div>
<ChevronDown <ChevronDown
@ -1075,17 +1179,17 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
openSections["fieldGroups"] && "rotate-180", openSections["fieldGroups"] && "rotate-180",
)} )}
/> />
</button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3"> <CollapsibleContent className="space-y-2 pt-2">
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
(: 거래처 , (: 거래처 , )
)
</p> </p>
{localFieldGroups.map((group, index) => ( {localFieldGroups.map((group, index) => (
<div key={group.id} className="space-y-2 rounded-lg border p-3"> <div key={group.id} className="space-y-2 rounded-md border p-2.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-medium"> <span className="text-xs font-medium truncate">
{index + 1}: {group.title} {index + 1}: {group.title}
</span> </span>
<Button <Button
@ -1093,20 +1197,17 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => removeFieldGroup(group.id)} onClick={() => removeFieldGroup(group.id)}
className="h-5 w-5 p-0 text-destructive" className="h-5 w-5 p-0 text-destructive shrink-0"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> ID</Label> <Label className="text-[10px]"> ID</Label>
<Input <Input
value={group.id} value={group.id}
onChange={(e) => onChange={(e) => updateFieldGroup(group.id, { id: e.target.value })}
updateFieldGroup(group.id, { id: e.target.value })
}
className="h-7 text-xs" className="h-7 text-xs"
/> />
</div> </div>
@ -1114,24 +1215,17 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Input <Input
value={group.title} value={group.title}
onChange={(e) => onChange={(e) => updateFieldGroup(group.id, { title: e.target.value })}
updateFieldGroup(group.id, { title: e.target.value })
}
className="h-7 text-xs" className="h-7 text-xs"
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"></Label> <Label className="text-[10px]"></Label>
<Input <Input
value={group.description || ""} value={group.description || ""}
onChange={(e) => onChange={(e) => updateFieldGroup(group.id, { description: e.target.value })}
updateFieldGroup(group.id, {
description: e.target.value,
})
}
placeholder="그룹 설명" placeholder="그룹 설명"
className="h-7 text-xs" className="h-7 text-xs"
/> />
@ -1141,41 +1235,27 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
<Input <Input
type="number" type="number"
value={group.order || 0} value={group.order || 0}
onChange={(e) => onChange={(e) => updateFieldGroup(group.id, { order: parseInt(e.target.value) || 0 })}
updateFieldGroup(group.id, {
order: parseInt(e.target.value) || 0,
})
}
className="h-7 text-xs" className="h-7 text-xs"
min="0" min="0"
/> />
</div> </div>
</div> </div>
{/* 소스 테이블 (그룹별) */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> ()</Label> <Label className="text-[10px]"> ()</Label>
<TableCombobox <TableCombobox
value={group.sourceTable || ""} value={group.sourceTable || ""}
tables={allTables} tables={allTables}
placeholder="기본 대상 테이블 사용" placeholder="기본 대상 테이블 사용"
onSelect={(v) => onSelect={(v) => updateFieldGroup(group.id, { sourceTable: v })}
updateFieldGroup(group.id, { sourceTable: v })
}
/> />
</div> </div>
{/* 최대 항목 수 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Input <Input
type="number" type="number"
value={group.maxEntries || ""} value={group.maxEntries || ""}
onChange={(e) => onChange={(e) => updateFieldGroup(group.id, { maxEntries: parseInt(e.target.value) || undefined })}
updateFieldGroup(group.id, {
maxEntries: parseInt(e.target.value) || undefined,
})
}
placeholder="무제한" placeholder="무제한"
className="h-7 w-20 text-xs" className="h-7 w-20 text-xs"
min="1" min="1"
@ -1197,19 +1277,23 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* ─── 부모 데이터 매핑 (Collapsible) ─── */} {/* ─── 부모 데이터 매핑 (서브 Collapsible) ─── */}
<Collapsible <Collapsible
open={openSections["parentMapping"] ?? false} open={openSections["parentMapping"] ?? false}
onOpenChange={() => toggleSection("parentMapping")} onOpenChange={() => toggleSection("parentMapping")}
> >
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border px-3 py-2 transition-colors hover:bg-muted/50"> <CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-md border px-3 py-2 transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link2 className="h-3.5 w-3.5 text-muted-foreground" /> <Link2 className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
{parentMappings.length > 0 && ( {parentMappings.length > 0 && (
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary"> <Badge variant="secondary" className="text-[10px] h-5">
{parentMappings.length} {parentMappings.length}
</span> </Badge>
)} )}
</div> </div>
<ChevronDown <ChevronDown
@ -1218,25 +1302,23 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
openSections["parentMapping"] && "rotate-180", openSections["parentMapping"] && "rotate-180",
)} )}
/> />
</button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3"> <CollapsibleContent className="space-y-2 pt-2">
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
( ) ( )
</p> </p>
{parentMappings.map((mapping, index) => { {parentMappings.map((mapping, index) => {
const isAutoDetected = autoDetectedFks.some( const isAutoDetected = autoDetectedFks.some(
(fk) => (fk) => fk.mappingType === "parent" && fk.columnName === mapping.targetField,
fk.mappingType === "parent" &&
fk.columnName === mapping.targetField,
); );
return ( return (
<div <div
key={index} key={index}
className={cn( className={cn(
"space-y-2 rounded-lg border p-3", "space-y-2 rounded-md border p-2.5",
isAutoDetected && isAutoDetected && "border-amber-200 bg-amber-50/30 dark:border-orange-800 dark:bg-orange-950/30",
"border-amber-200 bg-amber-50/30 dark:border-orange-800 dark:bg-orange-950/30",
)} )}
> >
{isAutoDetected && ( {isAutoDetected && (
@ -1244,8 +1326,6 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
FK FK
</span> </span>
)} )}
{/* 소스 테이블 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<TableCombobox <TableCombobox
@ -1253,16 +1333,11 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
tables={allTables} tables={allTables}
placeholder="테이블 선택" placeholder="테이블 선택"
onSelect={(v) => { onSelect={(v) => {
updateParentMapping(index, { updateParentMapping(index, { sourceTable: v, sourceField: "" });
sourceTable: v,
sourceField: "",
});
loadMappingColumns(v, index); loadMappingColumns(v, index);
}} }}
/> />
</div> </div>
{/* 원본 필드 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<ColumnCombobox <ColumnCombobox
@ -1270,13 +1345,9 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
columns={mappingSourceColumns[index] || []} columns={mappingSourceColumns[index] || []}
placeholder="컬럼 선택" placeholder="컬럼 선택"
disabled={!mapping.sourceTable} disabled={!mapping.sourceTable}
onSelect={(name) => onSelect={(name) => updateParentMapping(index, { sourceField: name })}
updateParentMapping(index, { sourceField: name })
}
/> />
</div> </div>
{/* 저장 필드 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> ( )</Label> <Label className="text-[10px]"> ( )</Label>
<ColumnCombobox <ColumnCombobox
@ -1284,21 +1355,13 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
columns={targetColumns} columns={targetColumns}
placeholder="저장 컬럼 선택" placeholder="저장 컬럼 선택"
disabled={!config.targetTable} disabled={!config.targetTable}
onSelect={(name) => onSelect={(name) => updateParentMapping(index, { targetField: name })}
updateParentMapping(index, { targetField: name })
}
/> />
</div> </div>
{/* 기본값 + 삭제 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
value={mapping.defaultValue || ""} value={mapping.defaultValue || ""}
onChange={(e) => onChange={(e) => updateParentMapping(index, { defaultValue: e.target.value || undefined })}
updateParentMapping(index, {
defaultValue: e.target.value || undefined,
})
}
placeholder="기본값 (선택)" placeholder="기본값 (선택)"
className="h-7 flex-1 text-xs" className="h-7 flex-1 text-xs"
/> />
@ -1329,19 +1392,21 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* ─── 자동 계산 설정 (Collapsible) ─── */} {/* ─── 자동 계산 (서브 Collapsible) ─── */}
<Collapsible <Collapsible
open={openSections["autoCalc"] ?? false} open={openSections["autoCalc"] ?? false}
onOpenChange={() => toggleSection("autoCalc")} onOpenChange={() => toggleSection("autoCalc")}
> >
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border px-3 py-2 transition-colors hover:bg-muted/50"> <CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-md border px-3 py-2 transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calculator className="h-3.5 w-3.5 text-muted-foreground" /> <Calculator className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span> <span className="text-xs font-medium"> </span>
{config.autoCalculation && ( {config.autoCalculation && (
<span className="rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary"> <Badge variant="secondary" className="text-[10px] h-5"></Badge>
</span>
)} )}
</div> </div>
<ChevronDown <ChevronDown
@ -1350,8 +1415,9 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
openSections["autoCalc"] && "rotate-180", openSections["autoCalc"] && "rotate-180",
)} )}
/> />
</button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3"> <CollapsibleContent className="space-y-2 pt-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-xs"> </Label> <Label className="text-xs"> </Label>
<Switch <Switch
@ -1381,42 +1447,30 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
{config.autoCalculation && ( {config.autoCalculation && (
<div className="space-y-2"> <div className="space-y-2">
{/* 계산 모드 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Select <Select
value={config.autoCalculation.mode || "template"} value={config.autoCalculation.mode || "template"}
onValueChange={(v: "template" | "custom") => onValueChange={(v: "template" | "custom") =>
handleChange("autoCalculation", { handleChange("autoCalculation", { ...config.autoCalculation, mode: v })
...config.autoCalculation,
mode: v,
})
} }
> >
<SelectTrigger className="h-7 text-xs"> <SelectTrigger className="h-7 text-xs">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="template" className="text-xs"> <SelectItem value="template" className="text-xs">릿 ( )</SelectItem>
릿 ( ) <SelectItem value="custom" className="text-xs"> ( )</SelectItem>
</SelectItem>
<SelectItem value="custom" className="text-xs">
( )
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* 계산 결과 필드 */}
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Select <Select
value={config.autoCalculation.targetField || ""} value={config.autoCalculation.targetField || ""}
onValueChange={(v) => onValueChange={(v) =>
handleChange("autoCalculation", { handleChange("autoCalculation", { ...config.autoCalculation, targetField: v })
...config.autoCalculation,
targetField: v,
})
} }
> >
<SelectTrigger className="h-7 text-xs"> <SelectTrigger className="h-7 text-xs">
@ -1424,11 +1478,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{localFields.map((f) => ( {localFields.map((f) => (
<SelectItem <SelectItem key={f.name} value={f.name} className="text-xs">
key={f.name}
value={f.name}
className="text-xs"
>
{f.label || f.name} {f.label || f.name}
</SelectItem> </SelectItem>
))} ))}
@ -1436,12 +1486,9 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</Select> </Select>
</div> </div>
{/* 템플릿 모드 필드 매핑 */}
{config.autoCalculation.mode === "template" && ( {config.autoCalculation.mode === "template" && (
<div className="space-y-2 rounded-md border p-2"> <div className="space-y-2 rounded-md border p-2">
<span className="text-[10px] font-medium"> <span className="text-[10px] font-medium"> </span>
</span>
{( {(
[ [
["basePrice", "기준 단가"], ["basePrice", "기준 단가"],
@ -1452,20 +1499,13 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
] as const ] as const
).map(([key, label]) => ( ).map(([key, label]) => (
<div key={key} className="flex items-center gap-2"> <div key={key} className="flex items-center gap-2">
<span className="w-20 text-[10px] text-muted-foreground"> <span className="w-20 text-[10px] text-muted-foreground truncate">{label}</span>
{label}
</span>
<Select <Select
value={ value={config.autoCalculation?.inputFields?.[key] || ""}
config.autoCalculation?.inputFields?.[key] || ""
}
onValueChange={(v) => onValueChange={(v) =>
handleChange("autoCalculation", { handleChange("autoCalculation", {
...config.autoCalculation, ...config.autoCalculation,
inputFields: { inputFields: { ...config.autoCalculation?.inputFields, [key]: v },
...config.autoCalculation?.inputFields,
[key]: v,
},
}) })
} }
> >
@ -1474,11 +1514,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{localFields.map((f) => ( {localFields.map((f) => (
<SelectItem <SelectItem key={f.name} value={f.name} className="text-[10px]">
key={f.name}
value={f.name}
className="text-[10px]"
>
{f.label || f.name} {f.label || f.name}
</SelectItem> </SelectItem>
))} ))}
@ -1493,85 +1529,6 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
{/* ─── 고급 설정 (Collapsible) ─── */}
<Collapsible
open={openSections["advanced"] ?? false}
onOpenChange={() => toggleSection("advanced")}
>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border px-3 py-2 transition-colors hover:bg-muted/50">
<div className="flex items-center gap-2">
<Settings className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium"> </span>
</div>
<ChevronDown
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
openSections["advanced"] && "rotate-180",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3">
{/* sourceKeyField */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<ColumnCombobox
value={config.sourceKeyField || ""}
columns={targetColumns}
placeholder="자동 감지 (entity FK)"
onSelect={(name) => handleChange("sourceKeyField", name)}
/>
<p className="text-[10px] text-muted-foreground">
FK
</p>
</div>
{/* 항목 번호 표시 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.showIndex ?? true}
onCheckedChange={(v) => handleChange("showIndex", v)}
/>
</div>
{/* 항목 삭제 허용 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.allowRemove ?? false}
onCheckedChange={(v) => handleChange("allowRemove", v)}
/>
</div>
{/* 비활성화 */}
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Switch
checked={config.disabled ?? false}
onCheckedChange={(v) => handleChange("disabled", v)}
/>
</div>
{/* 읽기 전용 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={config.readonly ?? false}
onCheckedChange={(v) => handleChange("readonly", v)}
/>
</div>
{/* 빈 상태 메시지 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={config.emptyMessage || ""}
onChange={(e) =>
handleChange("emptyMessage", e.target.value)
}
placeholder="전달받은 데이터가 없습니다."
className="h-8 text-xs"
/>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>

View File

@ -49,7 +49,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove }
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import type { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types"; import type { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
// ─── DnD 정렬 가능한 컬럼 행 ─── // ─── DnD 정렬 가능한 컬럼 행 (접이식) ───
function SortableColumnRow({ function SortableColumnRow({
id, id,
col, col,
@ -69,40 +69,57 @@ function SortableColumnRow({
}) { }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition }; const style = { transform: CSS.Transform.toString(transform), transition };
const [expanded, setExpanded] = useState(false);
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className={cn( className={cn(
"bg-card flex items-center gap-1.5 rounded-md border px-2 py-1.5", "bg-card rounded-md border px-2.5 py-1.5",
isDragging && "z-50 opacity-50 shadow-md", isDragging && "z-50 opacity-50 shadow-md",
isEntityJoin && "border-primary/20 bg-primary/5", isEntityJoin && "border-primary/20 bg-primary/5",
)} )}
> >
<div className="flex items-center gap-1.5">
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none"> <div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
<GripVertical className="h-3 w-3" /> <GripVertical className="h-3 w-3" />
</div> </div>
{isEntityJoin ? ( {isEntityJoin ? (
<Link2 className="h-3 w-3 shrink-0 text-primary" /> <Link2 className="h-3 w-3 shrink-0 text-primary" />
) : ( ) : (
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span> <span className="text-muted-foreground text-[10px] font-medium">#{index + 1}</span>
)} )}
<button
type="button"
className="truncate text-xs flex-1 text-left hover:underline"
onClick={() => setExpanded(!expanded)}
>
{col.displayName || col.columnName}
</button>
{col.width && (
<Badge variant="secondary" className="text-[10px] h-5 shrink-0">{col.width}px</Badge>
)}
<Button type="button" variant="ghost" size="sm" onClick={onRemove} className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0">
<X className="h-3 w-3" />
</Button>
</div>
{expanded && (
<div className="grid grid-cols-[1fr_60px] gap-1.5 pl-5 mt-1.5">
<Input <Input
value={col.displayName || col.columnName} value={col.displayName || col.columnName}
onChange={(e) => onLabelChange(e.target.value)} onChange={(e) => onLabelChange(e.target.value)}
placeholder="표시명" placeholder="표시명"
className="h-6 min-w-0 flex-1 text-xs" className="h-7 min-w-0 text-xs"
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)} onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
placeholder="너비" placeholder="너비"
className="h-6 w-14 shrink-0 text-xs" className="h-7 shrink-0 text-xs text-center"
/> />
<Button type="button" variant="ghost" size="sm" onClick={onRemove} className="text-muted-foreground hover:text-destructive h-5 w-5 shrink-0 p-0"> </div>
<X className="h-3 w-3" /> )}
</Button>
</div> </div>
); );
} }
@ -230,6 +247,11 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
// Collapsible 상태 // Collapsible 상태
const [advancedOpen, setAdvancedOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false);
const [entityDisplayOpen, setEntityDisplayOpen] = useState(false); const [entityDisplayOpen, setEntityDisplayOpen] = useState(false);
const [columnSelectOpen, setColumnSelectOpen] = useState(() => (config.columns?.length || 0) > 0);
const [entityJoinOpen, setEntityJoinOpen] = useState(false);
const [displayColumnsOpen, setDisplayColumnsOpen] = useState(() => (config.columns?.length || 0) > 0);
const [columnSearchText, setColumnSearchText] = useState("");
const [entityJoinSubOpen, setEntityJoinSubOpen] = useState<Record<number, boolean>>({});
// 이전 컬럼 개수 추적 (엔티티 감지용) // 이전 컬럼 개수 추적 (엔티티 감지용)
const prevColumnsLengthRef = useRef<number>(0); const prevColumnsLengthRef = useRef<number>(0);
@ -740,16 +762,45 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
</div> </div>
{/* ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ */}
{/* 2단계: 컬럼 선택 */} {/* 2단계: 컬럼 선택 (Collapsible) */}
{/* ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ */}
{targetTableName && availableColumns.length > 0 && ( {targetTableName && availableColumns.length > 0 && (
<> <>
<div className="space-y-3"> <Collapsible open={columnSelectOpen} onOpenChange={setColumnSelectOpen}>
<SectionHeader icon={Columns3} title="컬럼 선택" description="표시할 컬럼을 선택하세요" /> <CollapsibleTrigger asChild>
<Separator /> <button
type="button"
<div className="max-h-48 space-y-0.5 overflow-y-auto rounded-md border p-2"> 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"
{availableColumns.map((column) => { >
<div className="flex items-center gap-2">
<Columns3 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?.filter((c) => !c.isEntityJoin && !c.additionalJoinInfo).length || 0}
</Badge>
</div>
<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-2">
<Input
value={columnSearchText}
onChange={(e) => setColumnSearchText(e.target.value)}
placeholder="컬럼 검색..."
className="h-7 text-xs"
/>
<div className="max-h-[250px] space-y-0.5 overflow-y-auto">
{availableColumns
.filter((column) => {
if (!columnSearchText) return true;
const search = columnSearchText.toLowerCase();
return (
column.columnName.toLowerCase().includes(search) ||
(column.label || "").toLowerCase().includes(search)
);
})
.map((column) => {
const isAdded = config.columns?.some((c) => c.columnName === column.columnName); const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
return ( return (
<div <div
@ -818,23 +869,57 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
})} })}
</div> </div>
</div> </div>
</CollapsibleContent>
</Collapsible>
{/* Entity 조인 컬럼 */} {/* Entity 조인 컬럼 (Collapsible) */}
{entityJoinColumns.joinTables.length > 0 && ( {entityJoinColumns.joinTables.length > 0 && (
<div className="space-y-3"> <Collapsible open={entityJoinOpen} onOpenChange={setEntityJoinOpen}>
<SectionHeader icon={Link2} title="Entity 조인 컬럼" description="연관 테이블의 컬럼을 선택하세요" /> <CollapsibleTrigger asChild>
<Separator /> <button
<div className="space-y-3"> type="button"
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => ( 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 key={tableIndex} className="space-y-1"> >
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary"> <div className="flex items-center gap-2">
<Link2 className="h-3 w-3" /> <Link2 className="h-4 w-4 text-muted-foreground" />
<span>{joinTable.tableName}</span> <span className="text-sm font-medium">Entity </span>
<Badge variant="outline" className="text-[10px]"> <Badge variant="secondary" className="text-[10px] h-5">
{joinTable.currentDisplayColumn} {entityJoinColumns.joinTables.length}
</Badge> </Badge>
</div> </div>
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/5 p-2"> <ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", entityJoinOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3 space-y-2">
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => {
const addedCount = joinTable.availableColumns.filter((col) => {
const match = entityJoinColumns.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === col.columnName,
);
return match && config.columns?.some((c) => c.columnName === match.joinAlias);
}).length;
const isSubOpen = entityJoinSubOpen[tableIndex] ?? false;
return (
<Collapsible key={tableIndex} open={isSubOpen} onOpenChange={(open) => setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}>
<CollapsibleTrigger asChild>
<button
type="button"
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 className="flex items-center gap-2">
<Link2 className="h-3 w-3 text-primary" />
<span className="truncate text-xs font-medium">{joinTable.tableName}</span>
<Badge variant="secondary" className="text-[10px] h-5">
{addedCount > 0 ? `${addedCount}/${joinTable.availableColumns.length}개 선택` : `${joinTable.availableColumns.length}개 컬럼`}
</Badge>
</div>
<ChevronDown className={cn("h-3 w-3 text-muted-foreground transition-transform duration-200", isSubOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="max-h-[150px] 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) => { {joinTable.availableColumns.map((column, colIndex) => {
const matchingJoinColumn = entityJoinColumns.availableColumns.find( const matchingJoinColumn = entityJoinColumns.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
@ -879,10 +964,13 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
); );
})} })}
</div> </div>
</CollapsibleContent>
</Collapsible>
);
})}
</div> </div>
))} </CollapsibleContent>
</div> </Collapsible>
</div>
)} )}
</> </>
)} )}
@ -905,16 +993,28 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
)} )}
{/* ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ */}
{/* 3단계: 선택된 컬럼 순서 (DnD) */} {/* 3단계: 표시할 컬럼 (Collapsible + DnD) */}
{/* ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ */}
{config.columns && config.columns.length > 0 && ( {config.columns && config.columns.length > 0 && (
<div className="space-y-3"> <Collapsible open={displayColumnsOpen} onOpenChange={setDisplayColumnsOpen}>
<SectionHeader <CollapsibleTrigger asChild>
icon={GripVertical} <button
title={`표시할 컬럼 (${config.columns.length}개 선택)`} type="button"
description="드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다" 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"
/> >
<Separator /> <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", displayColumnsOpen && "rotate-180")} />
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-3">
<p className="text-[10px] text-muted-foreground mb-2"> , / </p>
<DndContext <DndContext
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={(event: DragEndEvent) => { onDragEnd={(event: DragEndEvent) => {
@ -934,7 +1034,7 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
items={(config.columns || []).map((c) => c.columnName)} items={(config.columns || []).map((c) => c.columnName)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-1"> <div className="max-h-[300px] space-y-1 overflow-y-auto">
{(config.columns || []).map((column, idx) => { {(config.columns || []).map((column, idx) => {
const resolvedLabel = const resolvedLabel =
column.displayName && column.displayName !== column.columnName column.displayName && column.displayName !== column.columnName
@ -958,6 +1058,8 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
</SortableContext> </SortableContext>
</DndContext> </DndContext>
</div> </div>
</CollapsibleContent>
</Collapsible>
)} )}
{/* ═══════════════════════════════════════ */} {/* ═══════════════════════════════════════ */}