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:
parent
b293d184bb
commit
13b2ebaf1f
|
|
@ -359,20 +359,10 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
||||||
return `${actualHeight}px`;
|
return `${actualHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 런타임 모드에서 컴포넌트 타입별 높이 처리
|
// 런타임 모드: ResponsiveGridRenderer가 ratio 기반으로 래퍼 높이를 설정하므로,
|
||||||
|
// 안쪽 컴포넌트는 "100%"로 래퍼를 채워야 비율이 정확하게 맞음
|
||||||
if (!isDesignMode) {
|
if (!isDesignMode) {
|
||||||
const compType = (component as any).componentType || component.componentConfig?.type || "";
|
const compType = (component as any).componentType || component.componentConfig?.type || "";
|
||||||
// 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1)
|
|
||||||
const flexGrowTypes = [
|
|
||||||
"table-list", "v2-table-list",
|
|
||||||
"split-panel-layout", "split-panel-layout2",
|
|
||||||
"v2-split-panel-layout", "screen-split-panel",
|
|
||||||
"v2-tab-container", "tab-container",
|
|
||||||
"tabs-widget", "v2-tabs-widget",
|
|
||||||
];
|
|
||||||
if (flexGrowTypes.some(t => compType === t)) {
|
|
||||||
return "100%";
|
|
||||||
}
|
|
||||||
const autoHeightTypes = [
|
const autoHeightTypes = [
|
||||||
"table-search-widget", "v2-table-search-widget",
|
"table-search-widget", "v2-table-search-widget",
|
||||||
"flow-widget",
|
"flow-widget",
|
||||||
|
|
@ -380,9 +370,11 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
||||||
if (autoHeightTypes.some(t => compType === t || compType.includes(t))) {
|
if (autoHeightTypes.some(t => compType === t || compType.includes(t))) {
|
||||||
return "auto";
|
return "auto";
|
||||||
}
|
}
|
||||||
|
// 나머지 모든 타입: 래퍼의 비율 스케일링을 따르도록 100%
|
||||||
|
return "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1순위: size.height가 있으면 우선 사용
|
// 디자인 모드: 고정 픽셀 사용 (캔버스 내 절대 좌표 배치)
|
||||||
if (size?.height && size.height > 0) {
|
if (size?.height && size.height > 0) {
|
||||||
if (component.componentConfig?.type === "table-list") {
|
if (component.componentConfig?.type === "table-list") {
|
||||||
return `${Math.max(size.height, 200)}px`;
|
return `${Math.max(size.height, 200)}px`;
|
||||||
|
|
@ -390,17 +382,14 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
||||||
return `${size.height}px`;
|
return `${size.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2순위: componentStyle.height (컴포넌트 정의에서 온 기본 스타일)
|
|
||||||
if (componentStyle?.height) {
|
if (componentStyle?.height) {
|
||||||
return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height;
|
return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3순위: 기본값
|
|
||||||
if (component.componentConfig?.type === "table-list") {
|
if (component.componentConfig?.type === "table-list") {
|
||||||
return "200px";
|
return "200px";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기본 높이
|
|
||||||
return "10px";
|
return "10px";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1410,7 +1410,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
const buttonElementStyle: React.CSSProperties = {
|
const buttonElementStyle: React.CSSProperties = {
|
||||||
width: buttonWidth,
|
width: buttonWidth,
|
||||||
height: buttonHeight,
|
height: buttonHeight,
|
||||||
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
|
minHeight: undefined, // 비율 스케일링 시 래퍼 높이를 정확히 따르도록 제거
|
||||||
// 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함)
|
// 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함)
|
||||||
borderWidth: style?.borderWidth || "0",
|
borderWidth: style?.borderWidth || "0",
|
||||||
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"),
|
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
FileSpreadsheet,
|
FileSpreadsheet,
|
||||||
List,
|
List,
|
||||||
PanelRight,
|
PanelRight,
|
||||||
|
GripVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
|
@ -313,10 +314,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
|
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
|
||||||
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
|
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
|
||||||
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
|
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
|
||||||
// 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 순서 변경)
|
// 우측 패널 컬럼 헤더 드래그 (디자인 + 런타임 순서 변경)
|
||||||
const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState<number | null>(null);
|
const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState<number | null>(null);
|
||||||
const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState<number | null>(null);
|
const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState<number | null>(null);
|
||||||
const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null);
|
const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null);
|
||||||
|
const [runtimeColumnOrder, setRuntimeColumnOrder] = useState<Record<string, number[]>>({});
|
||||||
|
|
||||||
// 데이터 상태
|
// 데이터 상태
|
||||||
const [leftData, setLeftData] = useState<any[]>([]);
|
const [leftData, setLeftData] = useState<any[]>([]);
|
||||||
|
|
@ -2544,55 +2546,67 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
handleRightColumnDragEnd();
|
handleRightColumnDragEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!onUpdateComponent) {
|
|
||||||
handleRightColumnDragEnd();
|
if (onUpdateComponent) {
|
||||||
return;
|
// 디자인 모드: config에 영구 저장
|
||||||
}
|
const rightPanel = componentConfig.rightPanel || {};
|
||||||
const rightPanel = componentConfig.rightPanel || {};
|
if (source === "main") {
|
||||||
if (source === "main") {
|
const allColumns = rightPanel.columns || [];
|
||||||
const allColumns = rightPanel.columns || [];
|
const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false);
|
||||||
const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false);
|
const hiddenColumns = 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) {
|
||||||
if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) {
|
handleRightColumnDragEnd();
|
||||||
handleRightColumnDragEnd();
|
return;
|
||||||
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 {
|
} else {
|
||||||
const tabs = [...(rightPanel.additionalTabs || [])];
|
// 런타임 모드: 로컬 상태로 순서 변경
|
||||||
const tabConfig = tabs[source];
|
const key = String(source);
|
||||||
if (!tabConfig || !Array.isArray(tabConfig.columns)) {
|
setRuntimeColumnOrder((prev) => {
|
||||||
handleRightColumnDragEnd();
|
const existing = prev[key];
|
||||||
return;
|
const maxLen = 100;
|
||||||
}
|
const order = existing || Array.from({ length: maxLen }, (_, i) => i);
|
||||||
const allTabCols = tabConfig.columns;
|
const reordered = [...order];
|
||||||
const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false);
|
const [removed] = reordered.splice(fromIdx, 1);
|
||||||
const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false);
|
reordered.splice(targetIndex, 0, removed);
|
||||||
if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) {
|
return { ...prev, [key]: reordered };
|
||||||
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 },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleRightColumnDragEnd();
|
handleRightColumnDragEnd();
|
||||||
|
|
@ -2604,9 +2618,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
component,
|
component,
|
||||||
onUpdateComponent,
|
onUpdateComponent,
|
||||||
handleRightColumnDragEnd,
|
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 handleEditModalSave = useCallback(async () => {
|
||||||
const tableName =
|
const tableName =
|
||||||
|
|
@ -3946,11 +3980,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{resizable && (
|
{resizable && (
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
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="분할선 드래그"
|
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/30 transition-opacity group-hover:opacity-70" />
|
||||||
<div className="h-7 w-0.5 rounded-full bg-muted-foreground/40 transition-colors group-hover:bg-primary-foreground/80" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -4107,7 +4140,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||||
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||||
const tabIndex = activeTabIndex - 1;
|
const tabIndex = activeTabIndex - 1;
|
||||||
const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent;
|
const canDragTabColumns = tabSummaryColumns.length > 0;
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|
@ -4120,7 +4153,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className={cn(
|
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",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4131,6 +4164,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
onDragEnd={handleRightColumnDragEnd}
|
onDragEnd={handleRightColumnDragEnd}
|
||||||
onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)}
|
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}
|
{col.label || col.name}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
|
|
@ -4158,7 +4192,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"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}`)}
|
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||||
>
|
>
|
||||||
|
|
@ -4243,7 +4277,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||||
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||||
const listTabIndex = activeTabIndex - 1;
|
const listTabIndex = activeTabIndex - 1;
|
||||||
const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent;
|
const canDragListTabColumns = listSummaryColumns.length > 0;
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|
@ -4256,7 +4290,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className={cn(
|
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",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4267,6 +4301,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
onDragEnd={handleRightColumnDragEnd}
|
onDragEnd={handleRightColumnDragEnd}
|
||||||
onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)}
|
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}
|
{col.label || col.name}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
|
|
@ -4293,7 +4328,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"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}`)}
|
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% 초과 시 스크롤)
|
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
|
||||||
const rightTotalColWidth = columnsToShow.reduce((sum, col) => {
|
const rightTotalColWidth = columnsToShow.reduce((sum, col) => {
|
||||||
const w = col.width && col.width <= 100 ? col.width : 0;
|
const w = col.width && col.width <= 100 ? col.width : 0;
|
||||||
|
|
@ -4653,7 +4696,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length;
|
const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length;
|
||||||
const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent;
|
const canDragRightColumns = displayColumns.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
|
|
@ -4670,7 +4713,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className={cn(
|
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",
|
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||||
isDraggable && "cursor-grab active:cursor-grabbing",
|
isDraggable && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
|
|
@ -4685,6 +4728,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
onDragEnd={handleRightColumnDragEnd}
|
onDragEnd={handleRightColumnDragEnd}
|
||||||
onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")}
|
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}
|
{col.label}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
|
|
@ -4707,7 +4751,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false;
|
||||||
|
|
||||||
return (
|
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) => (
|
{columnsToShow.map((col, colIdx) => (
|
||||||
<td
|
<td
|
||||||
key={colIdx}
|
key={colIdx}
|
||||||
|
|
@ -4851,7 +4895,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<tr
|
<tr
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/action cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"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)}
|
onClick={() => toggleRightItemExpansion(itemId)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
>
|
>
|
||||||
<TableHeader
|
<TableHeader
|
||||||
className={cn("border-b border-border/60", tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm")}
|
className={cn("border-b border-border/60", tableConfig?.stickyHeader && "sticky top-0 z-30 shadow-sm")}
|
||||||
style={{ backgroundColor: "hsl(var(--muted) / 0.8)" }}
|
style={{ backgroundColor: "hsl(var(--muted) / 0.4)" }}
|
||||||
>
|
>
|
||||||
<TableRow className="border-b border-border/60">
|
<TableRow className="border-b border-border/60">
|
||||||
{actualColumns.map((column, colIndex) => {
|
{actualColumns.map((column, colIndex) => {
|
||||||
|
|
@ -136,7 +136,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"
|
? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"
|
||||||
: "text-muted-foreground hover:text-foreground h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-[10px] font-bold uppercase tracking-[0.04em] whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-xs",
|
: "text-muted-foreground hover:text-foreground h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-[10px] font-bold uppercase tracking-[0.04em] whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-xs",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-muted/70",
|
column.sortable && "hover:bg-muted/50",
|
||||||
// 고정 컬럼 스타일
|
// 고정 컬럼 스타일
|
||||||
column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm",
|
column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm",
|
||||||
column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm",
|
column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm",
|
||||||
|
|
@ -151,7 +151,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
|
||||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
backgroundColor: "hsl(var(--muted) / 0.4)",
|
||||||
// sticky 위치 설정
|
// sticky 위치 설정
|
||||||
...(column.fixed === "left" && { left: leftFixedWidth }),
|
...(column.fixed === "left" && { left: leftFixedWidth }),
|
||||||
...(column.fixed === "right" && { right: rightFixedWidth }),
|
...(column.fixed === "right" && { right: rightFixedWidth }),
|
||||||
|
|
@ -230,7 +230,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||||
key={`row-${index}`}
|
key={`row-${index}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
index % 2 === 0 ? "bg-background" : "bg-muted/20",
|
||||||
tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent",
|
tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick?.(row, index, e)}
|
onClick={(e) => handleRowClick?.(row, index, e)}
|
||||||
|
|
|
||||||
|
|
@ -5819,8 +5819,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
{/* 🆕 Multi-Level Headers (Column Bands) */}
|
{/* 🆕 Multi-Level Headers (Column Bands) */}
|
||||||
{columnBandsInfo?.hasBands && (
|
{columnBandsInfo?.hasBands && (
|
||||||
<tr
|
<tr
|
||||||
className="border-primary/10 bg-muted/70 h-8 border-b sm:h-10"
|
className="border-border/60 bg-muted/40 h-8 border-b sm:h-10"
|
||||||
style={{ backgroundColor: "hsl(var(--muted) / 0.7)" }}
|
style={{ backgroundColor: "hsl(var(--muted) / 0.4)" }}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, colIdx) => {
|
{visibleColumns.map((column, colIdx) => {
|
||||||
// 이 컬럼이 속한 band 찾기
|
// 이 컬럼이 속한 band 찾기
|
||||||
|
|
@ -5863,7 +5863,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
<tr
|
<tr
|
||||||
className="bg-muted/80 h-10 border-b border-border/60 sm:h-12"
|
className="bg-muted/80 h-10 border-b border-border/60 sm:h-12"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
backgroundColor: "hsl(var(--muted) / 0.4)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, columnIndex) => {
|
{visibleColumns.map((column, columnIndex) => {
|
||||||
|
|
@ -5895,7 +5895,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2",
|
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2",
|
||||||
column.sortable !== false &&
|
column.sortable !== false &&
|
||||||
column.columnName !== "__checkbox__" &&
|
column.columnName !== "__checkbox__" &&
|
||||||
"hover:text-foreground hover:bg-muted/70 cursor-pointer transition-colors",
|
"hover:text-foreground hover:bg-muted/50 cursor-pointer transition-colors",
|
||||||
sortColumn === column.columnName && "!text-primary",
|
sortColumn === column.columnName && "!text-primary",
|
||||||
isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
|
isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
|
||||||
// 🆕 Column Reordering 스타일
|
// 🆕 Column Reordering 스타일
|
||||||
|
|
@ -5916,7 +5916,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
backgroundColor: "hsl(var(--muted) / 0.4)",
|
||||||
...(isFrozen && { left: `${leftPosition}px` }),
|
...(isFrozen && { left: `${leftPosition}px` }),
|
||||||
}}
|
}}
|
||||||
// 🆕 Column Reordering 이벤트
|
// 🆕 Column Reordering 이벤트
|
||||||
|
|
@ -6167,7 +6167,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
index % 2 === 0 ? "bg-background" : "bg-muted/20",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleRowClick(row, index, e)}
|
onClick={(e) => handleRowClick(row, index, e)}
|
||||||
>
|
>
|
||||||
|
|
@ -6306,9 +6306,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
|
||||||
index % 2 === 0 ? "bg-background" : "bg-muted/70",
|
index % 2 === 0 ? "bg-background" : "bg-muted/20",
|
||||||
isRowSelected && "!bg-primary/15 hover:!bg-primary/20",
|
isRowSelected && "!bg-primary/10 hover:!bg-primary/15",
|
||||||
isRowSelected && "[&_td]:!border-b-primary/30",
|
|
||||||
isRowFocused && "ring-primary/50 ring-1 ring-inset",
|
isRowFocused && "ring-primary/50 ring-1 ring-inset",
|
||||||
isDragEnabled && "cursor-grab active:cursor-grabbing",
|
isDragEnabled && "cursor-grab active:cursor-grabbing",
|
||||||
isDragging && "bg-muted opacity-50",
|
isDragging && "bg-muted opacity-50",
|
||||||
|
|
@ -6566,7 +6565,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
: undefined,
|
: undefined,
|
||||||
...(isFrozen && {
|
...(isFrozen && {
|
||||||
left: `${leftPosition}px`,
|
left: `${leftPosition}px`,
|
||||||
backgroundColor: "hsl(var(--muted) / 0.8)",
|
backgroundColor: "hsl(var(--muted) / 0.4)",
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue