[agent-pipeline] pipe-20260317063830-0nfs round-1
This commit is contained in:
@@ -10,7 +10,6 @@ import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
GripVertical,
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -21,6 +20,8 @@ import {
|
||||
Settings,
|
||||
Move,
|
||||
FileSpreadsheet,
|
||||
List,
|
||||
LayoutPanelRight,
|
||||
} from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
@@ -325,6 +326,10 @@ 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 [leftData, setLeftData] = useState<any[]>([]);
|
||||
@@ -2631,6 +2636,95 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}
|
||||
}, [selectedLeftItem, customLeftSelectedData, componentConfig, companyCode, toast, loadLeftData]);
|
||||
|
||||
// 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경)
|
||||
const handleRightColumnDragStart = useCallback(
|
||||
(columnIndex: number, source: "main" | number) => {
|
||||
setRightDraggedColumnIndex(columnIndex);
|
||||
setRightDragSource(source);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const handleRightColumnDragOver = useCallback((e: React.DragEvent, columnIndex: number) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
setRightDropTargetColumnIndex(columnIndex);
|
||||
}, []);
|
||||
const handleRightColumnDragEnd = useCallback(() => {
|
||||
setRightDraggedColumnIndex(null);
|
||||
setRightDropTargetColumnIndex(null);
|
||||
setRightDragSource(null);
|
||||
}, []);
|
||||
const handleRightColumnDrop = useCallback(
|
||||
(e: React.DragEvent, targetIndex: number, source: "main" | number) => {
|
||||
e.preventDefault();
|
||||
const fromIdx = rightDraggedColumnIndex;
|
||||
if (fromIdx === null || rightDragSource !== source || fromIdx === targetIndex) {
|
||||
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;
|
||||
}
|
||||
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 },
|
||||
},
|
||||
});
|
||||
}
|
||||
handleRightColumnDragEnd();
|
||||
},
|
||||
[
|
||||
rightDraggedColumnIndex,
|
||||
rightDragSource,
|
||||
componentConfig,
|
||||
component,
|
||||
onUpdateComponent,
|
||||
handleRightColumnDragEnd,
|
||||
],
|
||||
);
|
||||
|
||||
// 수정 모달 저장
|
||||
const handleEditModalSave = useCallback(async () => {
|
||||
const tableName =
|
||||
@@ -3212,10 +3306,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
||||
</CardTitle>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<List className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<CardTitle className="truncate text-base font-semibold">
|
||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
||||
</CardTitle>
|
||||
{!isDesignMode && (
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
{summedLeftData.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{!isDesignMode && (componentConfig.leftPanel as any)?.showBomExcelUpload && (
|
||||
<Button size="sm" variant="outline" onClick={() => setBomExcelUploadOpen(true)}>
|
||||
@@ -4011,13 +4113,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 리사이저 */}
|
||||
{/* 리사이저: 6px 너비, 그립 핸들(2x28px bar), hover 시 primary 하이라이트 */}
|
||||
{resizable && (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className="group bg-border hover:bg-primary flex w-1 cursor-col-resize items-center justify-center transition-colors"
|
||||
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"
|
||||
aria-label="분할선 드래그"
|
||||
>
|
||||
<GripVertical className="text-muted-foreground group-hover:text-primary-foreground h-4 w-4" />
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -4037,9 +4141,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-0">
|
||||
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 */}
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<LayoutPanelRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */}
|
||||
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
||||
<div className="flex items-center gap-0">
|
||||
<button
|
||||
@@ -4069,10 +4174,19 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CardTitle className="text-base font-semibold">
|
||||
<CardTitle className="truncate text-base font-semibold">
|
||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||
</CardTitle>
|
||||
)}
|
||||
{!isDesignMode && (
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
{activeTabIndex === 0
|
||||
? Array.isArray(rightData)
|
||||
? rightData.length
|
||||
: rightData ? 1 : 0
|
||||
: (tabsData[activeTabIndex]?.length ?? 0)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!isDesignMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -4163,16 +4277,35 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
|
||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||
const tabIndex = activeTabIndex - 1;
|
||||
const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent;
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{tabSummaryColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
))}
|
||||
{tabSummaryColumns.map((col: any, idx: number) => {
|
||||
const isDropTarget = rightDragSource === tabIndex && rightDropTargetColumnIndex === idx;
|
||||
const isDragging = rightDragSource === tabIndex && rightDraggedColumnIndex === idx;
|
||||
return (
|
||||
<th
|
||||
key={col.name}
|
||||
className={cn(
|
||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
|
||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||
canDragTabColumns && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
draggable={canDragTabColumns}
|
||||
onDragStart={() => canDragTabColumns && handleRightColumnDragStart(idx, tabIndex)}
|
||||
onDragOver={(e) => canDragTabColumns && handleRightColumnDragOver(e, idx)}
|
||||
onDragEnd={handleRightColumnDragEnd}
|
||||
onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)}
|
||||
>
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
{hasTabActions && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
||||
)}
|
||||
@@ -4280,16 +4413,35 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
|
||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||
const listTabIndex = activeTabIndex - 1;
|
||||
const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent;
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{listSummaryColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
))}
|
||||
{listSummaryColumns.map((col: any, idx: number) => {
|
||||
const isDropTarget = rightDragSource === listTabIndex && rightDropTargetColumnIndex === idx;
|
||||
const isDragging = rightDragSource === listTabIndex && rightDraggedColumnIndex === idx;
|
||||
return (
|
||||
<th
|
||||
key={col.name}
|
||||
className={cn(
|
||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold",
|
||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||
canDragListTabColumns && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
draggable={canDragListTabColumns}
|
||||
onDragStart={() => canDragListTabColumns && handleRightColumnDragStart(idx, listTabIndex)}
|
||||
onDragOver={(e) => canDragListTabColumns && handleRightColumnDragOver(e, idx)}
|
||||
onDragEnd={handleRightColumnDragEnd}
|
||||
onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)}
|
||||
>
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
{hasTabActions && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
||||
)}
|
||||
@@ -4672,24 +4824,43 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
return sum + w;
|
||||
}, 0);
|
||||
|
||||
const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length;
|
||||
const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="table-fixed" style={{ width: rightTotalColWidth > 100 ? `${rightTotalColWidth}%` : '100%' }}>
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
||||
style={{
|
||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||
textAlign: col.align || "left",
|
||||
}}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{columnsToShow.map((col, idx) => {
|
||||
const configColIndex = idx - rightConfigColumnStart;
|
||||
const isDraggable = canDragRightColumns && !col._isKeyColumn;
|
||||
const isDropTarget = rightDragSource === "main" && rightDropTargetColumnIndex === configColIndex;
|
||||
const isDragging = rightDragSource === "main" && rightDraggedColumnIndex === configColIndex;
|
||||
return (
|
||||
<th
|
||||
key={idx}
|
||||
className={cn(
|
||||
"text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap",
|
||||
isDropTarget && "border-l-[3px] border-l-primary bg-primary/5",
|
||||
isDraggable && "cursor-grab active:cursor-grabbing",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
style={{
|
||||
width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
|
||||
textAlign: col.align || "left",
|
||||
}}
|
||||
draggable={isDraggable}
|
||||
onDragStart={() => isDraggable && handleRightColumnDragStart(configColIndex, "main")}
|
||||
onDragOver={(e) => isDraggable && handleRightColumnDragOver(e, configColIndex)}
|
||||
onDragEnd={handleRightColumnDragEnd}
|
||||
onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */}
|
||||
{!isDesignMode &&
|
||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||
@@ -4705,7 +4876,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const itemId = item.id || item.ID || idx;
|
||||
|
||||
return (
|
||||
<tr key={itemId} className={cn("border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
|
||||
<tr key={itemId} className={cn("group/action border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
key={colIdx}
|
||||
@@ -4726,8 +4897,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
{!isDesignMode &&
|
||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||
<td className="px-3 py-2 text-right text-sm whitespace-nowrap">
|
||||
<div className="flex justify-end gap-1">
|
||||
<td className="px-3 py-2 text-right text-sm whitespace-nowrap group/action">
|
||||
<div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
||||
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
||||
<Button
|
||||
variant={
|
||||
@@ -4850,7 +5021,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
<React.Fragment key={itemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
"group/action cursor-pointer border-b border-border/40 transition-colors",
|
||||
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
@@ -4867,7 +5038,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
))}
|
||||
{hasActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover/action:opacity-100">
|
||||
{hasEditButton && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
@@ -4984,8 +5155,31 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
console.log(" ⚠️ 컬럼 설정 없음, 모든 컬럼 표시");
|
||||
}
|
||||
|
||||
const hasDetailEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
||||
const hasDetailDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{(hasDetailEditButton || hasDetailDeleteButton) && (
|
||||
<div className="flex items-center justify-end gap-1 pb-1">
|
||||
{hasDetailEditButton && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 px-2 text-xs"
|
||||
onClick={() => handleEditClick("right", rightData)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||
</Button>
|
||||
)}
|
||||
{hasDetailDeleteButton && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 gap-1 px-2 text-xs"
|
||||
onClick={() => handleDeleteClick("right", rightData)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
{componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{displayEntries.map(([key, value, label]) => (
|
||||
<div key={key} className="bg-card rounded-lg border p-4 shadow-sm">
|
||||
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
||||
|
||||
Reference in New Issue
Block a user