[agent-pipeline] pipe-20260311221723-l7a9 round-1
This commit is contained in:
parent
5093863e08
commit
8ad0c8797d
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ═══════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════ */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue