jskim-node #396
|
|
@ -2233,8 +2233,17 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
...component,
|
...component,
|
||||||
style: {
|
style: {
|
||||||
...component.style,
|
...component.style,
|
||||||
labelDisplay: false, // 상위에서 라벨을 표시했으므로 컴포넌트 내부에서는 숨김
|
labelDisplay: false,
|
||||||
|
labelPosition: "top" as const,
|
||||||
|
...(isHorizontalLabel ? { width: "100%", height: "100%" } : {}),
|
||||||
},
|
},
|
||||||
|
...(isHorizontalLabel ? {
|
||||||
|
size: {
|
||||||
|
...component.size,
|
||||||
|
width: undefined as unknown as number,
|
||||||
|
height: undefined as unknown as number,
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
}
|
}
|
||||||
: component;
|
: component;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1292,7 +1292,22 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const componentToRender = needsExternalLabel
|
const componentToRender = needsExternalLabel
|
||||||
? { ...splitAdjustedComponent, style: { ...splitAdjustedComponent.style, labelDisplay: false } }
|
? {
|
||||||
|
...splitAdjustedComponent,
|
||||||
|
style: {
|
||||||
|
...splitAdjustedComponent.style,
|
||||||
|
labelDisplay: false,
|
||||||
|
labelPosition: "top" as const,
|
||||||
|
...(isHorizLabel ? { width: "100%", height: "100%" } : {}),
|
||||||
|
},
|
||||||
|
...(isHorizLabel ? {
|
||||||
|
size: {
|
||||||
|
...splitAdjustedComponent.size,
|
||||||
|
width: undefined as unknown as number,
|
||||||
|
height: undefined as unknown as number,
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
|
}
|
||||||
: splitAdjustedComponent;
|
: splitAdjustedComponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -619,9 +619,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
componentType === "modal-repeater-table" ||
|
componentType === "modal-repeater-table" ||
|
||||||
componentType === "v2-input";
|
componentType === "v2-input";
|
||||||
|
|
||||||
// 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true일 때만 라벨 표시)
|
// 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true/"true"일 때만 라벨 표시)
|
||||||
const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay;
|
const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay;
|
||||||
const effectiveLabel = labelDisplay === true
|
const effectiveLabel = (labelDisplay === true || labelDisplay === "true")
|
||||||
? (component.style?.labelText || (component as any).label || component.componentConfig?.label)
|
? (component.style?.labelText || (component as any).label || component.componentConfig?.label)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,74 @@ import { TableListConfig, ColumnConfig } from "./types";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2, GripVertical, Pencil } from "lucide-react";
|
import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2, GripVertical, X } from "lucide-react";
|
||||||
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 { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
||||||
|
import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core";
|
||||||
|
import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 드래그 가능한 선택된 컬럼 행 (v2-split-panel-layout의 SortableColumnRow 동일 패턴)
|
||||||
|
*/
|
||||||
|
function SortableColumnRow({
|
||||||
|
id,
|
||||||
|
col,
|
||||||
|
index,
|
||||||
|
isEntityJoin,
|
||||||
|
onLabelChange,
|
||||||
|
onWidthChange,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
col: ColumnConfig;
|
||||||
|
index: number;
|
||||||
|
isEntityJoin?: boolean;
|
||||||
|
onLabelChange: (value: string) => void;
|
||||||
|
onWidthChange: (value: number) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
|
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
"bg-card flex items-center gap-1.5 rounded-md border px-2 py-1.5",
|
||||||
|
isDragging && "z-50 opacity-50 shadow-md",
|
||||||
|
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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-blue-500" />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface TableListConfigPanelProps {
|
export interface TableListConfigPanelProps {
|
||||||
config: TableListConfig;
|
config: TableListConfig;
|
||||||
|
|
@ -348,11 +411,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
||||||
if (existingColumn) return;
|
if (existingColumn) return;
|
||||||
|
|
||||||
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
|
// tableColumns → availableColumns 순서로 한국어 라벨 찾기
|
||||||
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
|
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
|
||||||
|
const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName);
|
||||||
|
|
||||||
// 라벨명 우선 사용, 없으면 컬럼명 사용
|
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
|
||||||
const displayName = columnInfo?.label || columnInfo?.displayName || columnName;
|
|
||||||
|
|
||||||
const newColumn: ColumnConfig = {
|
const newColumn: ColumnConfig = {
|
||||||
columnName,
|
columnName,
|
||||||
|
|
@ -1213,31 +1276,59 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 선택된 컬럼 순서 변경 */}
|
{/* 선택된 컬럼 순서 변경 (DnD) */}
|
||||||
{config.columns && config.columns.length > 0 && (
|
{config.columns && config.columns.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">컬럼 순서 / 설정</h3>
|
<h3 className="text-sm font-semibold">표시할 컬럼 ({config.columns.length}개 선택)</h3>
|
||||||
<p className="text-muted-foreground text-[10px]">
|
<p className="text-muted-foreground text-[10px]">
|
||||||
선택된 컬럼의 순서를 변경하거나 표시명을 수정할 수 있습니다
|
드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border" />
|
<hr className="border-border" />
|
||||||
|
<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; });
|
||||||
|
handleChange("columns", reordered);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={(config.columns || []).map((c) => c.columnName)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{[...(config.columns || [])]
|
{(config.columns || []).map((column, idx) => {
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
const resolvedLabel =
|
||||||
.map((column, idx) => (
|
column.displayName && column.displayName !== column.columnName
|
||||||
<SelectedColumnItem
|
? column.displayName
|
||||||
|
: availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
|
||||||
|
|
||||||
|
const colWithLabel = { ...column, displayName: resolvedLabel };
|
||||||
|
return (
|
||||||
|
<SortableColumnRow
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
column={column}
|
id={column.columnName}
|
||||||
|
col={colWithLabel}
|
||||||
index={idx}
|
index={idx}
|
||||||
total={config.columns?.length || 0}
|
isEntityJoin={!!column.isEntityJoin}
|
||||||
onMove={(direction) => moveColumn(column.columnName, direction)}
|
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||||
|
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||||
onRemove={() => removeColumn(column.columnName)}
|
onRemove={() => removeColumn(column.columnName)}
|
||||||
onUpdate={(updates) => updateColumn(column.columnName, updates)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1269,96 +1360,3 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 선택된 컬럼 항목 컴포넌트
|
|
||||||
* 순서 이동, 삭제, 표시명 수정 기능 제공
|
|
||||||
*/
|
|
||||||
const SelectedColumnItem: React.FC<{
|
|
||||||
column: ColumnConfig;
|
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
onMove: (direction: "up" | "down") => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
onUpdate: (updates: Partial<ColumnConfig>) => void;
|
|
||||||
}> = ({ column, index, total, onMove, onRemove, onUpdate }) => {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editValue, setEditValue] = useState(column.displayName || column.columnName);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const trimmed = editValue.trim();
|
|
||||||
if (trimmed && trimmed !== column.displayName) {
|
|
||||||
onUpdate({ displayName: trimmed });
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="hover:bg-muted/50 group flex items-center gap-1 rounded px-1.5 py-1">
|
|
||||||
<GripVertical className="text-muted-foreground/40 h-3 w-3 flex-shrink-0" />
|
|
||||||
|
|
||||||
<span className="text-muted-foreground min-w-[18px] text-[10px]">{index + 1}</span>
|
|
||||||
|
|
||||||
{isEditing ? (
|
|
||||||
<Input
|
|
||||||
value={editValue}
|
|
||||||
onChange={(e) => setEditValue(e.target.value)}
|
|
||||||
onBlur={handleSave}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") handleSave();
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
setEditValue(column.displayName || column.columnName);
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-5 flex-1 px-1 text-xs"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex flex-1 items-center gap-1 truncate text-left text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
setEditValue(column.displayName || column.columnName);
|
|
||||||
setIsEditing(true);
|
|
||||||
}}
|
|
||||||
title="클릭하여 표시명 수정"
|
|
||||||
>
|
|
||||||
<span className="truncate">{column.displayName || column.columnName}</span>
|
|
||||||
{column.isEntityJoin && (
|
|
||||||
<Link2 className="h-2.5 w-2.5 flex-shrink-0 text-blue-500" />
|
|
||||||
)}
|
|
||||||
<Pencil className="text-muted-foreground/0 group-hover:text-muted-foreground/60 ml-auto h-2.5 w-2.5 flex-shrink-0 transition-colors" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 items-center gap-0.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-muted disabled:opacity-30 rounded p-0.5 transition-colors"
|
|
||||||
onClick={() => onMove("up")}
|
|
||||||
disabled={index === 0}
|
|
||||||
title="위로 이동"
|
|
||||||
>
|
|
||||||
<ArrowUp className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-muted disabled:opacity-30 rounded p-0.5 transition-colors"
|
|
||||||
onClick={() => onMove("down")}
|
|
||||||
disabled={index === total - 1}
|
|
||||||
title="아래로 이동"
|
|
||||||
>
|
|
||||||
<ArrowDown className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded p-0.5 transition-colors"
|
|
||||||
onClick={onRemove}
|
|
||||||
title="컬럼 제거"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,75 @@ import {
|
||||||
Table2,
|
Table2,
|
||||||
Link2,
|
Link2,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Pencil,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
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 { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
||||||
|
import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core";
|
||||||
|
import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 드래그 가능한 선택된 컬럼 행 (v2-split-panel-layout의 SortableColumnRow 동일 패턴)
|
||||||
|
*/
|
||||||
|
function SortableColumnRow({
|
||||||
|
id,
|
||||||
|
col,
|
||||||
|
index,
|
||||||
|
isEntityJoin,
|
||||||
|
onLabelChange,
|
||||||
|
onWidthChange,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
col: ColumnConfig;
|
||||||
|
index: number;
|
||||||
|
isEntityJoin?: boolean;
|
||||||
|
onLabelChange: (value: string) => void;
|
||||||
|
onWidthChange: (value: number) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
|
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
"bg-card flex items-center gap-1.5 rounded-md border px-2 py-1.5",
|
||||||
|
isDragging && "z-50 opacity-50 shadow-md",
|
||||||
|
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<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-blue-500" />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface TableListConfigPanelProps {
|
export interface TableListConfigPanelProps {
|
||||||
config: TableListConfig;
|
config: TableListConfig;
|
||||||
|
|
@ -368,11 +431,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
||||||
if (existingColumn) return;
|
if (existingColumn) return;
|
||||||
|
|
||||||
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
|
// tableColumns → availableColumns 순서로 한국어 라벨 찾기
|
||||||
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
|
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
|
||||||
|
const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName);
|
||||||
|
|
||||||
// 라벨명 우선 사용, 없으면 컬럼명 사용
|
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
|
||||||
const displayName = columnInfo?.label || columnInfo?.displayName || columnName;
|
|
||||||
|
|
||||||
const newColumn: ColumnConfig = {
|
const newColumn: ColumnConfig = {
|
||||||
columnName,
|
columnName,
|
||||||
|
|
@ -1460,31 +1523,60 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 선택된 컬럼 순서 변경 */}
|
{/* 선택된 컬럼 순서 변경 (DnD) */}
|
||||||
{config.columns && config.columns.length > 0 && (
|
{config.columns && config.columns.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">컬럼 순서 / 설정</h3>
|
<h3 className="text-sm font-semibold">표시할 컬럼 ({config.columns.length}개 선택)</h3>
|
||||||
<p className="text-muted-foreground text-[10px]">
|
<p className="text-muted-foreground text-[10px]">
|
||||||
선택된 컬럼의 순서를 변경하거나 표시명을 수정할 수 있습니다
|
드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border" />
|
<hr className="border-border" />
|
||||||
|
<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; });
|
||||||
|
handleChange("columns", reordered);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={(config.columns || []).map((c) => c.columnName)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{[...(config.columns || [])]
|
{(config.columns || []).map((column, idx) => {
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
// displayName이 columnName과 같으면 한국어 라벨 미설정 → availableColumns에서 찾기
|
||||||
.map((column, idx) => (
|
const resolvedLabel =
|
||||||
<SelectedColumnItem
|
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}
|
key={column.columnName}
|
||||||
column={column}
|
id={column.columnName}
|
||||||
|
col={colWithLabel}
|
||||||
index={idx}
|
index={idx}
|
||||||
total={config.columns?.length || 0}
|
isEntityJoin={!!column.isEntityJoin}
|
||||||
onMove={(direction) => moveColumn(column.columnName, direction)}
|
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||||
|
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||||
onRemove={() => removeColumn(column.columnName)}
|
onRemove={() => removeColumn(column.columnName)}
|
||||||
onUpdate={(updates) => updateColumn(column.columnName, updates)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1515,96 +1607,3 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 선택된 컬럼 항목 컴포넌트
|
|
||||||
* 순서 이동, 삭제, 표시명 수정 기능 제공
|
|
||||||
*/
|
|
||||||
const SelectedColumnItem: React.FC<{
|
|
||||||
column: ColumnConfig;
|
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
onMove: (direction: "up" | "down") => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
onUpdate: (updates: Partial<ColumnConfig>) => void;
|
|
||||||
}> = ({ column, index, total, onMove, onRemove, onUpdate }) => {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editValue, setEditValue] = useState(column.displayName || column.columnName);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const trimmed = editValue.trim();
|
|
||||||
if (trimmed && trimmed !== column.displayName) {
|
|
||||||
onUpdate({ displayName: trimmed });
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="hover:bg-muted/50 group flex items-center gap-1 rounded px-1.5 py-1">
|
|
||||||
<GripVertical className="text-muted-foreground/40 h-3 w-3 flex-shrink-0" />
|
|
||||||
|
|
||||||
<span className="text-muted-foreground min-w-[18px] text-[10px]">{index + 1}</span>
|
|
||||||
|
|
||||||
{isEditing ? (
|
|
||||||
<Input
|
|
||||||
value={editValue}
|
|
||||||
onChange={(e) => setEditValue(e.target.value)}
|
|
||||||
onBlur={handleSave}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") handleSave();
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
setEditValue(column.displayName || column.columnName);
|
|
||||||
setIsEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-5 flex-1 px-1 text-xs"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex flex-1 items-center gap-1 truncate text-left text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
setEditValue(column.displayName || column.columnName);
|
|
||||||
setIsEditing(true);
|
|
||||||
}}
|
|
||||||
title="클릭하여 표시명 수정"
|
|
||||||
>
|
|
||||||
<span className="truncate">{column.displayName || column.columnName}</span>
|
|
||||||
{column.isEntityJoin && (
|
|
||||||
<Link2 className="h-2.5 w-2.5 flex-shrink-0 text-blue-500" />
|
|
||||||
)}
|
|
||||||
<Pencil className="text-muted-foreground/0 group-hover:text-muted-foreground/60 ml-auto h-2.5 w-2.5 flex-shrink-0 transition-colors" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-shrink-0 items-center gap-0.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-muted disabled:opacity-30 rounded p-0.5 transition-colors"
|
|
||||||
onClick={() => onMove("up")}
|
|
||||||
disabled={index === 0}
|
|
||||||
title="위로 이동"
|
|
||||||
>
|
|
||||||
<ArrowUp className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-muted disabled:opacity-30 rounded p-0.5 transition-colors"
|
|
||||||
onClick={() => onMove("down")}
|
|
||||||
disabled={index === total - 1}
|
|
||||||
title="아래로 이동"
|
|
||||||
>
|
|
||||||
<ArrowDown className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded p-0.5 transition-colors"
|
|
||||||
onClick={onRemove}
|
|
||||||
title="컬럼 제거"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue