[agent-pipeline] pipe-20260317063830-0nfs round-1

This commit is contained in:
DDD1542
2026-03-17 16:20:24 +09:00
parent 80cd95e683
commit 128872b766
8 changed files with 1013 additions and 518 deletions

View File

@@ -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">