jskim-node #396
|
|
@ -2233,8 +2233,17 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
|||
...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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1292,7 +1292,22 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
) : null;
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -619,9 +619,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
componentType === "modal-repeater-table" ||
|
||||
componentType === "v2-input";
|
||||
|
||||
// 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true일 때만 라벨 표시)
|
||||
// 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true/"true"일 때만 라벨 표시)
|
||||
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)
|
||||
: undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,74 @@ import { TableListConfig, ColumnConfig } from "./types";
|
|||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
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 { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 {
|
||||
config: TableListConfig;
|
||||
|
|
@ -348,11 +411,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
||||
if (existingColumn) return;
|
||||
|
||||
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
|
||||
// tableColumns → availableColumns 순서로 한국어 라벨 찾기
|
||||
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 || columnName;
|
||||
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
|
||||
|
||||
const newColumn: ColumnConfig = {
|
||||
columnName,
|
||||
|
|
@ -1213,31 +1276,59 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* 선택된 컬럼 순서 변경 */}
|
||||
{/* 선택된 컬럼 순서 변경 (DnD) */}
|
||||
{config.columns && config.columns.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
{[...(config.columns || [])]
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map((column, idx) => (
|
||||
<SelectedColumnItem
|
||||
{(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}
|
||||
column={column}
|
||||
id={column.columnName}
|
||||
col={colWithLabel}
|
||||
index={idx}
|
||||
total={config.columns?.length || 0}
|
||||
onMove={(direction) => moveColumn(column.columnName, direction)}
|
||||
isEntityJoin={!!column.isEntityJoin}
|
||||
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||
onRemove={() => removeColumn(column.columnName)}
|
||||
onUpdate={(updates) => updateColumn(column.columnName, updates)}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</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,
|
||||
Link2,
|
||||
GripVertical,
|
||||
Pencil,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
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 {
|
||||
config: TableListConfig;
|
||||
|
|
@ -368,11 +431,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
|
||||
if (existingColumn) return;
|
||||
|
||||
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
|
||||
// tableColumns → availableColumns 순서로 한국어 라벨 찾기
|
||||
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 || columnName;
|
||||
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
|
||||
|
||||
const newColumn: ColumnConfig = {
|
||||
columnName,
|
||||
|
|
@ -1460,31 +1523,60 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* 선택된 컬럼 순서 변경 */}
|
||||
{/* 선택된 컬럼 순서 변경 (DnD) */}
|
||||
{config.columns && config.columns.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
{[...(config.columns || [])]
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
.map((column, idx) => (
|
||||
<SelectedColumnItem
|
||||
{(config.columns || []).map((column, idx) => {
|
||||
// displayName이 columnName과 같으면 한국어 라벨 미설정 → availableColumns에서 찾기
|
||||
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}
|
||||
column={column}
|
||||
id={column.columnName}
|
||||
col={colWithLabel}
|
||||
index={idx}
|
||||
total={config.columns?.length || 0}
|
||||
onMove={(direction) => moveColumn(column.columnName, direction)}
|
||||
isEntityJoin={!!column.isEntityJoin}
|
||||
onLabelChange={(value) => updateColumn(column.columnName, { displayName: value })}
|
||||
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
|
||||
onRemove={() => removeColumn(column.columnName)}
|
||||
onUpdate={(updates) => updateColumn(column.columnName, updates)}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</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