feat: 분할 패널 내부 컴포넌트 선택 기능 추가

- RealtimePreviewDynamic, ScreenDesigner, DynamicComponentRenderer, SplitPanelLayoutComponent 및 관련 파일에서 분할 패널 내부 컴포넌트 선택 콜백 및 상태 관리 기능을 추가하였습니다.
- 커스텀 모드에서 패널 내부에 컴포넌트를 자유롭게 배치할 수 있는 기능을 구현하였습니다.
- 선택된 패널 컴포넌트의 상태를 관리하고, 관련 UI 요소를 업데이트하여 사용자 경험을 향상시켰습니다.
- 패널의 표시 모드에 'custom' 옵션을 추가하여 사용자 정의 배치 기능을 지원합니다.
This commit is contained in:
kjs
2026-01-30 16:34:05 +09:00
parent 152558d593
commit 17e212118c
11 changed files with 1814 additions and 50 deletions

View File

@@ -143,6 +143,9 @@ export interface DynamicComponentRendererProps {
// 🆕 탭 내부 컴포넌트 선택 콜백
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void;
selectedTabComponentId?: string;
// 🆕 분할 패널 내부 컴포넌트 선택 콜백
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void;
selectedPanelComponentId?: string;
flowSelectedStepId?: number | null;
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 새로고침 키
@@ -494,6 +497,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 🆕 탭 내부 컴포넌트 선택 콜백
onSelectTabComponent: props.onSelectTabComponent,
selectedTabComponentId: props.selectedTabComponentId,
// 🆕 분할 패널 내부 컴포넌트 선택 콜백
onSelectPanelComponent: props.onSelectPanelComponent,
selectedPanelComponentId: props.selectedPanelComponentId,
};
// 렌더러가 클래스인지 함수인지 확인

View File

@@ -18,6 +18,8 @@ import {
ChevronRight,
Pencil,
Trash2,
Settings,
Move,
} from "lucide-react";
import { dataApi } from "@/lib/api/data";
import { entityJoinApi } from "@/lib/api/entityJoin";
@@ -37,9 +39,16 @@ import { useTableOptions } from "@/contexts/TableOptionsContext";
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
import { useAuth } from "@/hooks/useAuth";
import { useSplitPanel } from "./SplitPanelContext";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { PanelInlineComponent } from "./types";
import { cn } from "@/lib/utils";
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
// 추가 props
onUpdateComponent?: (component: any) => void;
// 🆕 패널 내부 컴포넌트 선택 콜백 (탭 컴포넌트와 동일 구조)
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: PanelInlineComponent) => void;
selectedPanelComponentId?: string;
}
/**
@@ -52,6 +61,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
isSelected = false,
isPreview = false,
onClick,
onUpdateComponent,
onSelectPanelComponent,
selectedPanelComponentId: externalSelectedPanelComponentId,
...props
}) => {
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
@@ -181,6 +193,207 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [rightCategoryMappings, setRightCategoryMappings] = useState<
Record<string, Record<string, { label: string; color?: string }>>
>({}); // 우측 카테고리 매핑
// 🆕 커스텀 모드: 드래그/리사이즈 상태
const [draggingCompId, setDraggingCompId] = useState<string | null>(null);
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
// 🆕 외부에서 전달받은 선택 상태 사용 (탭 컴포넌트와 동일 구조)
const selectedPanelComponentId = externalSelectedPanelComponentId || null;
const rafRef = useRef<number | null>(null);
// 🆕 10px 단위 스냅 함수
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
// 🆕 커스텀 모드: 컴포넌트 삭제 핸들러
const handleRemovePanelComponent = useCallback(
(panelSide: "left" | "right", compId: string) => {
if (!onUpdateComponent) return;
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
const panelConfig = componentConfig[panelKey] || {};
const updatedComponents = (panelConfig.components || []).filter(
(c: PanelInlineComponent) => c.id !== compId
);
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
[panelKey]: {
...panelConfig,
components: updatedComponents,
},
},
});
},
[component, componentConfig, onUpdateComponent]
);
// 🆕 커스텀 모드: 드래그 시작 핸들러
const handlePanelDragStart = useCallback(
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent) => {
e.stopPropagation();
e.preventDefault();
const startMouseX = e.clientX;
const startMouseY = e.clientY;
const startLeft = comp.position?.x || 0;
const startTop = comp.position?.y || 0;
setDraggingCompId(comp.id);
setDragPosition({ x: startLeft, y: startTop });
const handleMouseMove = (moveEvent: MouseEvent) => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY;
const newX = Math.max(0, startLeft + deltaX);
const newY = Math.max(0, startTop + deltaY);
setDragPosition({ x: newX, y: newY });
});
};
const handleMouseUp = (upEvent: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY;
const newX = Math.max(0, startLeft + deltaX);
const newY = Math.max(0, startTop + deltaY);
setDraggingCompId(null);
setDragPosition(null);
if (onUpdateComponent) {
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
const panelConfig = componentConfig[panelKey] || {};
const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) =>
c.id === comp.id
? { ...c, position: { x: Math.round(newX), y: Math.round(newY) } }
: c
);
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
[panelKey]: {
...panelConfig,
components: updatedComponents,
},
},
});
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[component, componentConfig, onUpdateComponent]
);
// 🆕 커스텀 모드: 리사이즈 시작 핸들러
const handlePanelResizeStart = useCallback(
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent, direction: "e" | "s" | "se") => {
e.stopPropagation();
e.preventDefault();
const startMouseX = e.clientX;
const startMouseY = e.clientY;
const startWidth = comp.size?.width || 200;
const startHeight = comp.size?.height || 100;
setResizingCompId(comp.id);
setResizeSize({ width: startWidth, height: startHeight });
const handleMouseMove = (moveEvent: MouseEvent) => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY;
let newWidth = startWidth;
let newHeight = startHeight;
if (direction === "e" || direction === "se") {
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
}
if (direction === "s" || direction === "se") {
newHeight = snapTo10(Math.max(30, startHeight + deltaY));
}
setResizeSize({ width: newWidth, height: newHeight });
});
};
const handleMouseUp = (upEvent: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY;
let newWidth = startWidth;
let newHeight = startHeight;
if (direction === "e" || direction === "se") {
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
}
if (direction === "s" || direction === "se") {
newHeight = snapTo10(Math.max(30, startHeight + deltaY));
}
setResizingCompId(null);
setResizeSize(null);
if (onUpdateComponent) {
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
const panelConfig = componentConfig[panelKey] || {};
const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) =>
c.id === comp.id
? { ...c, size: { width: newWidth, height: newHeight } }
: c
);
onUpdateComponent({
...component,
componentConfig: {
...componentConfig,
[panelKey]: {
...panelConfig,
components: updatedComponents,
},
},
});
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[component, componentConfig, onUpdateComponent, snapTo10]
);
const { toast } = useToast();
// 추가 모달 상태
@@ -2079,8 +2292,191 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</div>
)}
<CardContent className="flex-1 overflow-auto p-4">
{/* 좌측 데이터 목록/테이블 */}
{componentConfig.leftPanel?.displayMode === "table" ? (
{/* 좌측 데이터 목록/테이블/커스텀 */}
{componentConfig.leftPanel?.displayMode === "custom" ? (
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
<div
className="relative h-full w-full"
data-split-panel-container="true"
data-component-id={component.id}
data-panel-side="left"
>
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
const isSelectedComp = selectedPanelComponentId === comp.id;
const isDraggingComp = draggingCompId === comp.id;
const isResizingComp = resizingCompId === comp.id;
// 드래그/리사이즈 중 표시할 크기/위치
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
const componentData = {
id: comp.id,
type: "component" as const,
componentType: comp.componentType,
label: comp.label,
position: comp.position || { x: 0, y: 0 },
size: { width: displayWidth, height: displayHeight },
componentConfig: comp.componentConfig || {},
style: comp.style || {},
};
if (isDesignMode) {
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
return (
<div
key={comp.id}
data-panel-comp-id={comp.id}
className="absolute"
style={{
left: displayX,
top: displayY,
zIndex: isDraggingComp ? 100 : isSelectedComp ? 10 : 1,
}}
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("left", comp.id, comp);
}}
>
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
<div
className={cn(
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
isSelectedComp ? "border-primary" : "border-gray-200"
)}
style={{ width: displayWidth }}
onMouseDown={(e) => handlePanelDragStart(e, "left", comp)}
>
<div className="flex items-center gap-0.5">
<Move className="h-2.5 w-2.5 text-gray-400" />
<span className="max-w-[100px] truncate text-[9px] text-gray-500">
{comp.label || comp.componentType}
</span>
</div>
<div className="flex items-center">
<button
className="rounded p-0.5 hover:bg-gray-200"
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("left", comp.id, comp);
}}
title="설정"
>
<Settings className="h-2.5 w-2.5 text-gray-500" />
</button>
<button
className="rounded p-0.5 hover:bg-red-100"
onClick={(e) => {
e.stopPropagation();
handleRemovePanelComponent("left", comp.id);
}}
title="삭제"
>
<Trash2 className="h-2.5 w-2.5 text-red-500" />
</button>
</div>
</div>
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
<div
className={cn(
"relative overflow-hidden rounded-b border bg-white shadow-sm",
isSelectedComp
? "border-primary ring-2 ring-primary/30"
: "border-gray-200",
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
!(isDraggingComp || isResizingComp) && "transition-all"
)}
style={{
width: displayWidth,
height: displayHeight,
}}
>
<div className="pointer-events-none h-full w-full">
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
/>
</div>
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
{isSelectedComp && (
<>
{/* 오른쪽 가장자리 (너비 조절) */}
<div
className="pointer-events-auto absolute right-0 top-0 z-10 h-full w-2 cursor-ew-resize hover:bg-primary/10"
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "e")}
/>
{/* 아래 가장자리 (높이 조절) */}
<div
className="pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize hover:bg-primary/10"
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "s")}
/>
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
<div
className="pointer-events-auto absolute bottom-0 right-0 z-20 h-3 w-3 cursor-nwse-resize hover:bg-primary/20"
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "se")}
/>
</>
)}
</div>
</div>
);
} else {
// 실행 모드: DynamicComponentRenderer로 렌더링
const componentData = {
id: comp.id,
type: "component" as const,
componentType: comp.componentType,
label: comp.label,
position: comp.position || { x: 0, y: 0 },
size: comp.size || { width: 400, height: 300 },
componentConfig: comp.componentConfig || {},
style: comp.style || {},
};
return (
<div
key={comp.id}
className="absolute"
style={{
left: comp.position?.x || 0,
top: comp.position?.y || 0,
width: comp.size?.width || 400,
height: comp.size?.height || 300,
}}
>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={false}
formData={{}}
/>
</div>
);
}
})}
</div>
) : (
// 컴포넌트가 없을 때 드롭 영역 표시
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
<Plus className="mb-2 h-8 w-8 text-gray-400" />
<p className="text-sm font-medium text-gray-500">
</p>
<p className="mt-1 text-xs text-gray-400">
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
</p>
</div>
)}
</div>
) : componentConfig.leftPanel?.displayMode === "table" ? (
// 테이블 모드
<div className="w-full">
{isDesignMode ? (
@@ -2577,8 +2973,180 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</div>
)}
<CardContent className="flex-1 overflow-auto p-4">
{/* 우측 데이터 */}
{isLoadingRight ? (
{/* 우측 데이터/커스텀 */}
{componentConfig.rightPanel?.displayMode === "custom" ? (
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
<div
className="relative h-full w-full"
data-split-panel-container="true"
data-component-id={component.id}
data-panel-side="right"
>
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {
const isSelectedComp = selectedPanelComponentId === comp.id;
const isDraggingComp = draggingCompId === comp.id;
const isResizingComp = resizingCompId === comp.id;
// 드래그/리사이즈 중 표시할 크기/위치
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
const componentData = {
id: comp.id,
type: "component" as const,
componentType: comp.componentType,
label: comp.label,
position: comp.position || { x: 0, y: 0 },
size: { width: displayWidth, height: displayHeight },
componentConfig: comp.componentConfig || {},
style: comp.style || {},
};
if (isDesignMode) {
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
return (
<div
key={comp.id}
data-panel-comp-id={comp.id}
className="absolute"
style={{
left: displayX,
top: displayY,
zIndex: isDraggingComp ? 100 : isSelectedComp ? 10 : 1,
}}
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("right", comp.id, comp);
}}
>
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
<div
className={cn(
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
isSelectedComp ? "border-primary" : "border-gray-200"
)}
style={{ width: displayWidth }}
onMouseDown={(e) => handlePanelDragStart(e, "right", comp)}
>
<div className="flex items-center gap-0.5">
<Move className="h-2.5 w-2.5 text-gray-400" />
<span className="max-w-[100px] truncate text-[9px] text-gray-500">
{comp.label || comp.componentType}
</span>
</div>
<div className="flex items-center">
<button
className="rounded p-0.5 hover:bg-gray-200"
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("right", comp.id, comp);
}}
title="설정"
>
<Settings className="h-2.5 w-2.5 text-gray-500" />
</button>
<button
className="rounded p-0.5 hover:bg-red-100"
onClick={(e) => {
e.stopPropagation();
handleRemovePanelComponent("right", comp.id);
}}
title="삭제"
>
<Trash2 className="h-2.5 w-2.5 text-red-500" />
</button>
</div>
</div>
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
<div
className={cn(
"relative overflow-hidden rounded-b border bg-white shadow-sm",
isSelectedComp
? "border-primary ring-2 ring-primary/30"
: "border-gray-200",
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
!(isDraggingComp || isResizingComp) && "transition-all"
)}
style={{
width: displayWidth,
height: displayHeight,
}}
>
<div className="pointer-events-none h-full w-full">
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
/>
</div>
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
{isSelectedComp && (
<>
{/* 오른쪽 가장자리 (너비 조절) */}
<div
className="pointer-events-auto absolute right-0 top-0 z-10 h-full w-2 cursor-ew-resize hover:bg-primary/10"
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "e")}
/>
{/* 아래 가장자리 (높이 조절) */}
<div
className="pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize hover:bg-primary/10"
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "s")}
/>
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
<div
className="pointer-events-auto absolute bottom-0 right-0 z-20 h-3 w-3 cursor-nwse-resize hover:bg-primary/20"
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "se")}
/>
</>
)}
</div>
</div>
);
} else {
return (
<div
key={comp.id}
className="absolute"
style={{
left: comp.position?.x || 0,
top: comp.position?.y || 0,
width: comp.size?.width || 400,
height: comp.size?.height || 300,
}}
>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={false}
formData={{}}
/>
</div>
);
}
})}
</div>
) : (
// 컴포넌트가 없을 때 드롭 영역 표시
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
<Plus className="mb-2 h-8 w-8 text-gray-400" />
<p className="text-sm font-medium text-gray-500">
</p>
<p className="mt-1 text-xs text-gray-400">
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
</p>
</div>
)}
</div>
) : isLoadingRight ? (
// 로딩 중
<div className="flex h-full items-center justify-center">
<div className="text-center">

View File

@@ -11,7 +11,8 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
// Accordion 제거 - 단순 섹션으로 변경
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical } from "lucide-react";
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical, Move } from "lucide-react";
import { PanelInlineComponent } from "./types";
import { cn } from "@/lib/utils";
import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types";
import { TableInfo, ColumnInfo } from "@/types/screen";
@@ -1547,7 +1548,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<Label> </Label>
<Select
value={config.leftPanel?.displayMode || "list"}
onValueChange={(value: "list" | "table") => updateLeftPanel({ displayMode: value })}
onValueChange={(value: "list" | "table" | "custom") => updateLeftPanel({ displayMode: value })}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="표시 모드 선택" />
@@ -1565,11 +1566,75 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
<SelectItem value="custom">
<div className="flex flex-col">
<span className="font-medium"> (CUSTOM)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
{config.leftPanel?.displayMode === "custom" && (
<p className="text-xs text-amber-600">
.
</p>
)}
</div>
{/* 좌측 패널 표시 컬럼 설정 - 체크박스 방식 */}
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */}
{config.leftPanel?.displayMode === "custom" && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{!config.leftPanel?.components || config.leftPanel.components.length === 0 ? (
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
<p className="text-muted-foreground text-xs">
</p>
</div>
) : (
<div className="space-y-2">
{config.leftPanel.components.map((comp: PanelInlineComponent) => (
<div
key={comp.id}
className="flex items-center justify-between rounded-md border bg-background p-2"
>
<div className="flex-1">
<p className="text-xs font-medium">
{comp.label || comp.componentType}
</p>
<p className="text-muted-foreground text-[10px]">
{comp.componentType} | : ({comp.position?.x || 0}, {comp.position?.y || 0}) | : {comp.size?.width || 0}x{comp.size?.height || 0}
</p>
</div>
<Button
onClick={() => {
const updatedComponents = (config.leftPanel?.components || []).filter(
(c: PanelInlineComponent) => c.id !== comp.id
);
onChange({
...config,
leftPanel: {
...config.leftPanel,
components: updatedComponents,
},
});
}}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
)}
{/* 좌측 패널 표시 컬럼 설정 - 체크박스 방식 (커스텀 모드가 아닐 때만) */}
{config.leftPanel?.displayMode !== "custom" && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
@@ -1731,6 +1796,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
</div>
)}
</div>
)}
</div>
{/* 좌측 패널 데이터 필터링 */}
@@ -1851,7 +1917,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<Label> </Label>
<Select
value={config.rightPanel?.displayMode || "list"}
onValueChange={(value: "list" | "table") => updateRightPanel({ displayMode: value })}
onValueChange={(value: "list" | "table" | "custom") => updateRightPanel({ displayMode: value })}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="표시 모드 선택" />
@@ -1869,11 +1935,74 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
<SelectItem value="custom">
<div className="flex flex-col">
<span className="font-medium"> (CUSTOM)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
{config.rightPanel?.displayMode === "custom" && (
<p className="text-xs text-amber-600">
.
</p>
)}
</div>
{/* 요약 표시 설정 (LIST 모드에서만) */}
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */}
{config.rightPanel?.displayMode === "custom" && (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{!config.rightPanel?.components || config.rightPanel.components.length === 0 ? (
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
<p className="text-muted-foreground text-xs">
</p>
</div>
) : (
<div className="space-y-2">
{config.rightPanel.components.map((comp: PanelInlineComponent) => (
<div
key={comp.id}
className="flex items-center justify-between rounded-md border bg-background p-2"
>
<div className="flex-1">
<p className="text-xs font-medium">
{comp.label || comp.componentType}
</p>
<p className="text-muted-foreground text-[10px]">
{comp.componentType} | : ({comp.position?.x || 0}, {comp.position?.y || 0}) | : {comp.size?.width || 0}x{comp.size?.height || 0}
</p>
</div>
<Button
onClick={() => {
const updatedComponents = (config.rightPanel?.components || []).filter(
(c: PanelInlineComponent) => c.id !== comp.id
);
onChange({
...config,
rightPanel: {
...config.rightPanel,
components: updatedComponents,
},
});
}}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
</div>
)}
{/* 요약 표시 설정 (LIST 모드에서만, 커스텀 모드가 아닐 때) */}
{(config.rightPanel?.displayMode || "list") === "list" && (
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
<Label className="text-sm font-semibold"> </Label>

View File

@@ -2,7 +2,13 @@
* SplitPanelLayout 컴포넌트 타입 정의
*/
import { DataFilterConfig } from "@/types/screen-management";
import { DataFilterConfig, TabInlineComponent } from "@/types/screen-management";
/**
* 패널 내 인라인 컴포넌트 (커스텀 모드용)
* TabInlineComponent와 동일한 구조 사용
*/
export type PanelInlineComponent = TabInlineComponent;
/**
* 추가 탭 설정 (우측 패널과 동일한 구조 + tabId, label)
@@ -118,7 +124,9 @@ export interface SplitPanelLayoutConfig {
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
dataSource?: string; // API 엔드포인트
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
displayMode?: "list" | "table" | "custom"; // 표시 모드: 목록, 테이블, 또는 커스텀
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 (탭 컴포넌트와 동일 구조)
components?: PanelInlineComponent[];
showSearch?: boolean;
showAdd?: boolean;
showEdit?: boolean; // 수정 버튼
@@ -185,7 +193,9 @@ export interface SplitPanelLayoutConfig {
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
dataSource?: string;
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
displayMode?: "list" | "table" | "custom"; // 표시 모드: 목록, 테이블, 또는 커스텀
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 (탭 컴포넌트와 동일 구조)
components?: PanelInlineComponent[];
showSearch?: boolean;
showAdd?: boolean;
showEdit?: boolean; // 수정 버튼