Refactor ColumnDetailPanel and AppLayout for improved loading state handling and UI consistency. Enhance TabBar and TableListComponent styles for better user experience. Update V2SplitPanelLayoutConfigPanel to manage button visibility based on configuration. Introduce filter chips in TableListComponent for better filter management.

This commit is contained in:
DDD1542
2026-03-17 22:02:52 +09:00
parent b293d184bb
commit 13b2ebaf1f
5 changed files with 124 additions and 92 deletions

View File

@@ -22,6 +22,7 @@ import {
FileSpreadsheet,
List,
PanelRight,
GripVertical,
} from "lucide-react";
import { dataApi } from "@/lib/api/data";
import { entityJoinApi } from "@/lib/api/entityJoin";
@@ -313,10 +314,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
// 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 순서 변경)
// 우측 패널 컬럼 헤더 드래그 (디자인 + 런타임 순서 변경)
const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState<number | null>(null);
const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState<number | null>(null);
const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null);
const [runtimeColumnOrder, setRuntimeColumnOrder] = useState<Record<string, number[]>>({});
// 데이터 상태
const [leftData, setLeftData] = useState<any[]>([]);
@@ -2544,55 +2546,67 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
handleRightColumnDragEnd();
return;
}
if (!onUpdateComponent) {
handleRightColumnDragEnd();
return;
}
const rightPanel = componentConfig.rightPanel || {};
if (source === "main") {
const allColumns = rightPanel.columns || [];
const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false);
const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) {
handleRightColumnDragEnd();
return;
if (onUpdateComponent) {
// 디자인 모드: config에 영구 저장
const rightPanel = componentConfig.rightPanel || {};
if (source === "main") {
const allColumns = rightPanel.columns || [];
const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false);
const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) {
handleRightColumnDragEnd();
return;
}
const reordered = [...visibleColumns];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenColumns];
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, columns },
},
});
} else {
const tabs = [...(rightPanel.additionalTabs || [])];
const tabConfig = tabs[source];
if (!tabConfig || !Array.isArray(tabConfig.columns)) {
handleRightColumnDragEnd();
return;
}
const allTabCols = tabConfig.columns;
const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false);
const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) {
handleRightColumnDragEnd();
return;
}
const reordered = [...visibleTabCols];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenTabCols];
const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t));
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, additionalTabs: newTabs },
},
});
}
const reordered = [...visibleColumns];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenColumns];
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, columns },
},
});
} else {
const tabs = [...(rightPanel.additionalTabs || [])];
const tabConfig = tabs[source];
if (!tabConfig || !Array.isArray(tabConfig.columns)) {
handleRightColumnDragEnd();
return;
}
const allTabCols = tabConfig.columns;
const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false);
const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false);
if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) {
handleRightColumnDragEnd();
return;
}
const reordered = [...visibleTabCols];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
const columns = [...reordered, ...hiddenTabCols];
const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t));
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
rightPanel: { ...rightPanel, additionalTabs: newTabs },
},
// 런타임 모드: 로컬 상태로 순서 변경
const key = String(source);
setRuntimeColumnOrder((prev) => {
const existing = prev[key];
const maxLen = 100;
const order = existing || Array.from({ length: maxLen }, (_, i) => i);
const reordered = [...order];
const [removed] = reordered.splice(fromIdx, 1);
reordered.splice(targetIndex, 0, removed);
return { ...prev, [key]: reordered };
});
}
handleRightColumnDragEnd();
@@ -2604,9 +2618,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
component,
onUpdateComponent,
handleRightColumnDragEnd,
setRuntimeColumnOrder,
],
);
// 런타임 컬럼 순서 적용 헬퍼
const applyRuntimeOrder = useCallback(
<T,>(columns: T[], source: "main" | number): T[] => {
const key = String(source);
const order = runtimeColumnOrder[key];
if (!order) return columns;
const result: T[] = [];
for (const idx of order) {
if (idx < columns.length) result.push(columns[idx]);
}
// order에 없는 나머지 컬럼 추가
for (let i = 0; i < columns.length; i++) {
if (!order.includes(i)) result.push(columns[i]);
}
return result.length > 0 ? result : columns;
},
[runtimeColumnOrder],
);
// 수정 모달 저장
const handleEditModalSave = useCallback(async () => {
const tableName =
@@ -3946,11 +3980,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{resizable && (
<div
onMouseDown={handleMouseDown}
className="group flex w-1.5 cursor-col-resize flex-col items-center justify-center gap-0.5 bg-border transition-colors hover:bg-primary"
className="group flex w-1.5 cursor-col-resize flex-col items-center justify-center gap-0.5 bg-border/60 transition-colors hover:bg-primary/25"
aria-label="분할선 드래그"
>
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/40 transition-colors group-hover:bg-primary-foreground/80" />
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/40 transition-colors group-hover:bg-primary-foreground/80" />
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/30 transition-opacity group-hover:opacity-70" />
</div>
)}
@@ -4107,7 +4140,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
const tabIndex = activeTabIndex - 1;
const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent;
const canDragTabColumns = tabSummaryColumns.length > 0;
return (
<div className="h-full overflow-auto">
<table className="w-full text-sm">
@@ -4120,7 +4153,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<th
key={col.name}
className={cn(
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
"group/th text-muted-foreground relative px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
canDragTabColumns && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
@@ -4131,6 +4164,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)}
>
{canDragTabColumns && <GripVertical className="text-muted-foreground/30 group-hover/th:text-muted-foreground/60 absolute top-1/2 left-0.5 h-3 w-3 -translate-y-1/2 transition-opacity" />}
{col.label || col.name}
</th>
);
@@ -4158,7 +4192,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<tr
className={cn(
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/20 hover:bg-accent" : "hover:bg-accent",
)}
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
>
@@ -4243,7 +4277,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
const listTabIndex = activeTabIndex - 1;
const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent;
const canDragListTabColumns = listSummaryColumns.length > 0;
return (
<div className="h-full overflow-auto">
<table className="w-full text-sm">
@@ -4256,7 +4290,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<th
key={col.name}
className={cn(
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
"group/th text-muted-foreground relative px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em]",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
@@ -4267,6 +4301,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)}
>
{canDragListTabColumns && <GripVertical className="text-muted-foreground/30 group-hover/th:text-muted-foreground/60 absolute top-1/2 left-0.5 h-3 w-3 -translate-y-1/2 transition-opacity" />}
{col.label || col.name}
</th>
);
@@ -4293,7 +4328,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<tr
className={cn(
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/20 hover:bg-accent" : "hover:bg-accent",
)}
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
>
@@ -4646,6 +4681,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}));
}
// 런타임 컬럼 순서 적용
if (!isDesignMode && runtimeColumnOrder["main"]) {
const keyColCount = columnsToShow.filter((c: any) => c._isKeyColumn).length;
const keyCols = columnsToShow.slice(0, keyColCount);
const dataCols = columnsToShow.slice(keyColCount);
columnsToShow = [...keyCols, ...applyRuntimeOrder(dataCols, "main")];
}
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
const rightTotalColWidth = columnsToShow.reduce((sum, col) => {
const w = col.width && col.width <= 100 ? col.width : 0;
@@ -4653,7 +4696,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}, 0);
const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length;
const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent;
const canDragRightColumns = displayColumns.length > 0;
return (
<div className="flex h-full w-full flex-col">
@@ -4670,7 +4713,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<th
key={idx}
className={cn(
"text-muted-foreground px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em] whitespace-nowrap",
"group/th text-muted-foreground relative px-3 py-[7px] text-left text-[9px] font-bold uppercase tracking-[0.04em] whitespace-nowrap",
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
isDraggable && "cursor-grab active:cursor-grabbing",
isDragging && "opacity-50",
@@ -4685,6 +4728,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
onDragEnd={handleRightColumnDragEnd}
onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")}
>
{isDraggable && <GripVertical className="text-muted-foreground/30 group-hover/th:text-muted-foreground/60 absolute top-1/2 left-0.5 h-3 w-3 -translate-y-1/2 transition-opacity" />}
{col.label}
</th>
);
@@ -4707,7 +4751,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
return (
<tr key={itemId} className={cn("group/action border-b border-border/50 transition-[background] duration-75 hover:bg-accent", idx % 2 === 1 && "bg-muted/50")}>
<tr key={itemId} className={cn("group/action border-b border-border/50 transition-[background] duration-75 hover:bg-accent", idx % 2 === 1 && "bg-muted/20")}>
{columnsToShow.map((col, colIdx) => (
<td
key={colIdx}
@@ -4851,7 +4895,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<tr
className={cn(
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/50 hover:bg-accent" : "hover:bg-accent",
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/20 hover:bg-accent" : "hover:bg-accent",
)}
onClick={() => toggleRightItemExpansion(itemId)}
>