[agent-pipeline] pipe-20260311221723-l7a9 round-1
This commit is contained in:
@@ -49,7 +49,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove }
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import type { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
|
||||
|
||||
// ─── DnD 정렬 가능한 컬럼 행 ───
|
||||
// ─── DnD 정렬 가능한 컬럼 행 (접이식) ───
|
||||
function SortableColumnRow({
|
||||
id,
|
||||
col,
|
||||
@@ -69,40 +69,57 @@ function SortableColumnRow({
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
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",
|
||||
isEntityJoin && "border-primary/20 bg-primary/5",
|
||||
)}
|
||||
>
|
||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||
<GripVertical className="h-3 w-3" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||
<GripVertical className="h-3 w-3" />
|
||||
</div>
|
||||
{isEntityJoin ? (
|
||||
<Link2 className="h-3 w-3 shrink-0 text-primary" />
|
||||
) : (
|
||||
<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>
|
||||
{isEntityJoin ? (
|
||||
<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>
|
||||
{expanded && (
|
||||
<div className="grid grid-cols-[1fr_60px] gap-1.5 pl-5 mt-1.5">
|
||||
<Input
|
||||
value={col.displayName || col.columnName}
|
||||
onChange={(e) => onLabelChange(e.target.value)}
|
||||
placeholder="표시명"
|
||||
className="h-7 min-w-0 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={col.width || ""}
|
||||
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
|
||||
placeholder="너비"
|
||||
className="h-7 shrink-0 text-xs text-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
value={col.displayName || col.columnName}
|
||||
onChange={(e) => onLabelChange(e.target.value)}
|
||||
placeholder="표시명"
|
||||
className="h-6 min-w-0 flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={col.width || ""}
|
||||
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
|
||||
placeholder="너비"
|
||||
className="h-6 w-14 shrink-0 text-xs"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -230,6 +247,11 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
|
||||
// Collapsible 상태
|
||||
const [advancedOpen, setAdvancedOpen] = 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);
|
||||
@@ -740,149 +762,215 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════ */}
|
||||
{/* 2단계: 컬럼 선택 */}
|
||||
{/* 2단계: 컬럼 선택 (Collapsible) */}
|
||||
{/* ═══════════════════════════════════════ */}
|
||||
{targetTableName && availableColumns.length > 0 && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<SectionHeader icon={Columns3} title="컬럼 선택" description="표시할 컬럼을 선택하세요" />
|
||||
<Separator />
|
||||
|
||||
<div className="max-h-48 space-y-0.5 overflow-y-auto rounded-md border p-2">
|
||||
{availableColumns.map((column) => {
|
||||
const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
|
||||
return (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
||||
isAdded && "bg-primary/10",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
|
||||
} else {
|
||||
addColumn(column.columnName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isAdded}
|
||||
onCheckedChange={() => {
|
||||
if (isAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
|
||||
} else {
|
||||
addColumn(column.columnName);
|
||||
}
|
||||
}}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate text-xs">{column.label || column.columnName}</span>
|
||||
{isAdded && (
|
||||
<button
|
||||
type="button"
|
||||
title={
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "편집 잠금 (클릭하여 해제)"
|
||||
: "편집 가능 (클릭하여 잠금)"
|
||||
}
|
||||
className={cn(
|
||||
"ml-auto flex-shrink-0 rounded p-0.5 transition-colors",
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const currentCol = config.columns?.find((c) => c.columnName === column.columnName);
|
||||
if (currentCol) {
|
||||
updateColumn(column.columnName, {
|
||||
editable: currentCol.editable === false ? undefined : false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{config.columns?.find((c) => c.columnName === column.columnName)?.editable === false ? (
|
||||
<Lock className="h-3 w-3" />
|
||||
) : (
|
||||
<Unlock className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span className={cn("text-[10px] text-muted-foreground/70", !isAdded && "ml-auto")}>
|
||||
{column.input_type || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entity 조인 컬럼 */}
|
||||
{entityJoinColumns.joinTables.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<SectionHeader icon={Link2} title="Entity 조인 컬럼" description="연관 테이블의 컬럼을 선택하세요" />
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
|
||||
<div key={tableIndex} className="space-y-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span>{joinTable.tableName}</span>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{joinTable.currentDisplayColumn}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/5 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
|
||||
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||
);
|
||||
const isAlreadyAdded = config.columns?.some(
|
||||
(col) => col.columnName === matchingJoinColumn?.joinAlias,
|
||||
);
|
||||
if (!matchingJoinColumn) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10",
|
||||
isAlreadyAdded && "bg-primary/10",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAlreadyAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||
<Collapsible open={columnSelectOpen} onOpenChange={setColumnSelectOpen}>
|
||||
<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">
|
||||
{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);
|
||||
return (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className={cn(
|
||||
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
||||
isAdded && "bg-primary/10",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
|
||||
} else {
|
||||
addColumn(column.columnName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isAdded}
|
||||
onCheckedChange={() => {
|
||||
if (isAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
|
||||
} else {
|
||||
addEntityColumn(matchingJoinColumn);
|
||||
addColumn(column.columnName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isAlreadyAdded}
|
||||
onCheckedChange={() => {
|
||||
if (isAlreadyAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||
} else {
|
||||
addEntityColumn(matchingJoinColumn);
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate text-xs">{column.label || column.columnName}</span>
|
||||
{isAdded && (
|
||||
<button
|
||||
type="button"
|
||||
title={
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "편집 잠금 (클릭하여 해제)"
|
||||
: "편집 가능 (클릭하여 잠금)"
|
||||
}
|
||||
className={cn(
|
||||
"ml-auto flex-shrink-0 rounded p-0.5 transition-colors",
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const currentCol = config.columns?.find((c) => c.columnName === column.columnName);
|
||||
if (currentCol) {
|
||||
updateColumn(column.columnName, {
|
||||
editable: currentCol.editable === false ? undefined : false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
{config.columns?.find((c) => c.columnName === column.columnName)?.editable === false ? (
|
||||
<Lock className="h-3 w-3" />
|
||||
) : (
|
||||
<Unlock className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span className={cn("text-[10px] text-muted-foreground/70", !isAdded && "ml-auto")}>
|
||||
{column.input_type || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Entity 조인 컬럼 (Collapsible) */}
|
||||
{entityJoinColumns.joinTables.length > 0 && (
|
||||
<Collapsible open={entityJoinOpen} onOpenChange={setEntityJoinOpen}>
|
||||
<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">
|
||||
<Link2 className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Entity 조인</span>
|
||||
<Badge variant="secondary" className="text-[10px] h-5">
|
||||
{entityJoinColumns.joinTables.length}개 테이블
|
||||
</Badge>
|
||||
</div>
|
||||
<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) => {
|
||||
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
|
||||
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
||||
);
|
||||
const isAlreadyAdded = config.columns?.some(
|
||||
(col) => col.columnName === matchingJoinColumn?.joinAlias,
|
||||
);
|
||||
if (!matchingJoinColumn) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10",
|
||||
isAlreadyAdded && "bg-primary/10",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAlreadyAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||
} else {
|
||||
addEntityColumn(matchingJoinColumn);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isAlreadyAdded}
|
||||
onCheckedChange={() => {
|
||||
if (isAlreadyAdded) {
|
||||
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
|
||||
} else {
|
||||
addEntityColumn(matchingJoinColumn);
|
||||
}
|
||||
}}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -905,59 +993,73 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════ */}
|
||||
{/* 3단계: 선택된 컬럼 순서 (DnD) */}
|
||||
{/* 3단계: 표시할 컬럼 (Collapsible + DnD) */}
|
||||
{/* ═══════════════════════════════════════ */}
|
||||
{config.columns && config.columns.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<SectionHeader
|
||||
icon={GripVertical}
|
||||
title={`표시할 컬럼 (${config.columns.length}개 선택)`}
|
||||
description="드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다"
|
||||
/>
|
||||
<Separator />
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const columns = [...(config.columns || [])];
|
||||
const oldIndex = columns.findIndex((c) => c.columnName === active.id);
|
||||
const newIndex = columns.findIndex((c) => c.columnName === over.id);
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const reordered = arrayMove(columns, oldIndex, newIndex);
|
||||
reordered.forEach((col, idx) => { col.order = idx; });
|
||||
updateField("columns", reordered);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SortableContext
|
||||
items={(config.columns || []).map((c) => c.columnName)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
<Collapsible open={displayColumnsOpen} onOpenChange={setDisplayColumnsOpen}>
|
||||
<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="space-y-1">
|
||||
{(config.columns || []).map((column, idx) => {
|
||||
const resolvedLabel =
|
||||
column.displayName && column.displayName !== column.columnName
|
||||
? column.displayName
|
||||
: availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
|
||||
const colWithLabel = { ...column, displayName: resolvedLabel };
|
||||
return (
|
||||
<SortableColumnRow
|
||||
key={column.columnName}
|
||||
id={column.columnName}
|
||||
col={colWithLabel}
|
||||
index={idx}
|
||||
isEntityJoin={!!column.isEntityJoin}
|
||||
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||
onRemove={() => removeColumn(column.columnName)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</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
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const columns = [...(config.columns || [])];
|
||||
const oldIndex = columns.findIndex((c) => c.columnName === active.id);
|
||||
const newIndex = columns.findIndex((c) => c.columnName === over.id);
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const reordered = arrayMove(columns, oldIndex, newIndex);
|
||||
reordered.forEach((col, idx) => { col.order = idx; });
|
||||
updateField("columns", reordered);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SortableContext
|
||||
items={(config.columns || []).map((c) => c.columnName)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="max-h-[300px] space-y-1 overflow-y-auto">
|
||||
{(config.columns || []).map((column, idx) => {
|
||||
const resolvedLabel =
|
||||
column.displayName && column.displayName !== column.columnName
|
||||
? column.displayName
|
||||
: availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
|
||||
const colWithLabel = { ...column, displayName: resolvedLabel };
|
||||
return (
|
||||
<SortableColumnRow
|
||||
key={column.columnName}
|
||||
id={column.columnName}
|
||||
col={colWithLabel}
|
||||
index={idx}
|
||||
isEntityJoin={!!column.isEntityJoin}
|
||||
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||
onRemove={() => removeColumn(column.columnName)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════ */}
|
||||
|
||||
Reference in New Issue
Block a user