Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-11 12:23:52 +09:00
588 changed files with 25062 additions and 13798 deletions

View File

@@ -44,6 +44,7 @@ import { useSplitPanel } from "./SplitPanelContext";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { PanelInlineComponent } from "./types";
import { cn } from "@/lib/utils";
import { ResponsiveGridRenderer } from "@/components/screen/ResponsiveGridRenderer";
import { BomExcelUploadModal } from "../v2-bom-tree/BomExcelUploadModal";
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
@@ -726,24 +727,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return `${height}px`; // 숫자면 px 추가
};
const componentStyle: React.CSSProperties = isPreview
const componentStyle: React.CSSProperties = isDesignMode
? {
// 반응형 모드: position relative, 그리드 컨테이너가 제공하는 크기 사용
position: "relative",
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤
height: getHeightValue(),
border: "1px solid #e5e7eb",
}
: {
// 디자이너 모드: position absolute
position: "absolute",
left: `${component.style?.positionX || 0}px`,
top: `${component.style?.positionY || 0}px`,
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤 (그리드 기반)
width: "100%",
height: getHeightValue(),
zIndex: component.style?.positionZ || 1,
cursor: isDesignMode ? "pointer" : "default",
cursor: "pointer",
border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb",
}
: {
position: "relative",
width: "100%",
height: getHeightValue(),
};
// 계층 구조 빌드 함수 (트리 구조 유지)
@@ -2993,13 +2991,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<div
ref={containerRef}
style={{
...(isPreview
? {
position: "relative",
height: `${component.style?.height || 600}px`,
border: "1px solid #e5e7eb",
}
: componentStyle),
...componentStyle,
display: "flex",
flexDirection: "row",
}}
@@ -3013,8 +3005,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
>
{/* 좌측 패널 */}
<div
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px`, height: "100%" }}
className="border-border flex flex-shrink-0 flex-col border-r"
style={{ width: `${leftWidth}%`, minWidth: isDesignMode ? `${minLeftWidth}px` : "0", height: "100%" }}
className="border-border flex flex-col border-r"
>
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
<CardHeader
@@ -3071,22 +3063,74 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
data-component-id={component.id}
data-panel-side="left"
>
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
!isDesignMode ? (
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
(() => {
const leftComps = componentConfig.leftPanel!.components;
const canvasW = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
const canvasH = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
const compDataList = leftComps.map((c: PanelInlineComponent) => ({
id: c.id,
type: "component" as const,
componentType: c.componentType,
label: c.label,
position: c.position || { x: 0, y: 0 },
size: c.size || { width: 400, height: 300 },
componentConfig: c.componentConfig || {},
style: c.style || {},
tableName: c.componentConfig?.tableName,
columnName: c.componentConfig?.columnName,
webType: c.componentConfig?.webType,
inputType: (c as any).inputType || c.componentConfig?.inputType,
})) as any;
return (
<ResponsiveGridRenderer
components={compDataList}
canvasWidth={canvasW}
canvasHeight={canvasH}
renderComponent={(comp) => (
<DynamicComponentRenderer
component={comp as any}
isDesignMode={false}
isInteractive={true}
formData={{}}
tableName={componentConfig.leftPanel?.tableName}
menuObjid={(props as any).menuObjid}
screenId={(props as any).screenId}
userId={(props as any).userId}
userName={(props as any).userName}
companyCode={companyCode}
allComponents={(props as any).allComponents}
selectedRowsData={localSelectedRowsData}
onSelectedRowsChange={handleLocalSelectedRowsChange}
onFormDataChange={(data: any) => {
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
setCustomLeftSelectedData(data.selectedRowsData[0]);
setSelectedLeftItem(data.selectedRowsData[0]);
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
setCustomLeftSelectedData({});
setSelectedLeftItem(null);
}
}}
/>
)}
/>
);
})()
) : (
<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 형식으로 변환
// componentConfig의 주요 속성을 최상위로 펼침 (일반 화면의 overrides 플래트닝과 동일)
const componentData = {
id: comp.id,
type: "component" as const,
@@ -3096,16 +3140,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
size: { width: displayWidth, height: displayHeight },
componentConfig: comp.componentConfig || {},
style: comp.style || {},
// 파일 업로드/미디어 등이 component.tableName, component.columnName을 직접 참조하므로 펼침
tableName: comp.componentConfig?.tableName,
columnName: comp.componentConfig?.columnName,
webType: comp.componentConfig?.webType,
inputType: comp.inputType || comp.componentConfig?.inputType,
inputType: (comp as any).inputType || comp.componentConfig?.inputType,
};
if (isDesignMode) {
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
return (
return (
<div
key={comp.id}
data-panel-comp-id={comp.id}
@@ -3127,38 +3168,38 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
<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"
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-muted px-1",
isSelectedComp ? "border-primary" : "border-border"
)}
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">
<Move className="h-2.5 w-2.5 text-muted-foreground/70" />
<span className="max-w-[100px] truncate text-[9px] text-muted-foreground">
{comp.label || comp.componentType}
</span>
</div>
<div className="flex items-center">
<button
className="rounded p-0.5 hover:bg-gray-200"
className="rounded p-0.5 hover:bg-muted/80"
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("left", comp.id, comp);
}}
title="설정"
>
<Settings className="h-2.5 w-2.5 text-gray-500" />
<Settings className="h-2.5 w-2.5 text-muted-foreground" />
</button>
<button
className="rounded p-0.5 hover:bg-red-100"
className="rounded p-0.5 hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
handleRemovePanelComponent("left", comp.id);
}}
title="삭제"
>
<Trash2 className="h-2.5 w-2.5 text-red-500" />
<Trash2 className="h-2.5 w-2.5 text-destructive" />
</button>
</div>
</div>
@@ -3169,7 +3210,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
"relative overflow-hidden rounded-b border bg-white shadow-sm",
isSelectedComp
? "border-primary ring-2 ring-primary/30"
: "border-gray-200",
: "border-border",
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
!(isDraggingComp || isResizingComp) && "transition-all"
)}
@@ -3242,68 +3283,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</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}
isInteractive={true}
formData={{}}
tableName={componentConfig.leftPanel?.tableName}
menuObjid={(props as any).menuObjid}
screenId={(props as any).screenId}
userId={(props as any).userId}
userName={(props as any).userName}
companyCode={companyCode}
allComponents={(props as any).allComponents}
selectedRowsData={localSelectedRowsData}
onSelectedRowsChange={handleLocalSelectedRowsChange}
onFormDataChange={(data: any) => {
// 커스텀 모드: 좌측 카드/테이블 선택 시 데이터 캡처
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
setCustomLeftSelectedData(data.selectedRowsData[0]);
setSelectedLeftItem(data.selectedRowsData[0]);
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
setCustomLeftSelectedData({});
setSelectedLeftItem(null);
}
}}
/>
</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">
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-input bg-muted/50">
<Plus className="mb-2 h-8 w-8 text-muted-foreground/70" />
<p className="text-sm font-medium text-muted-foreground">
</p>
<p className="mt-1 text-xs text-gray-400">
<p className="mt-1 text-xs text-muted-foreground/70">
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
</p>
</div>
@@ -3315,21 +3305,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{isDesignMode ? (
// 디자인 모드: 샘플 테이블
<div className="overflow-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase"> 1</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase"> 2</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase"> 3</th>
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase"> 1</th>
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase"> 2</th>
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase"> 3</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
<tr className="cursor-pointer hover:bg-gray-50">
<tbody className="divide-y divide-border bg-white">
<tr className="cursor-pointer hover:bg-muted">
<td className="px-3 py-2 text-sm whitespace-nowrap"> 1-1</td>
<td className="px-3 py-2 text-sm whitespace-nowrap"> 1-2</td>
<td className="px-3 py-2 text-sm whitespace-nowrap"> 1-3</td>
</tr>
<tr className="cursor-pointer hover:bg-gray-50">
<tr className="cursor-pointer hover:bg-muted">
<td className="px-3 py-2 text-sm whitespace-nowrap"> 2-1</td>
<td className="px-3 py-2 text-sm whitespace-nowrap"> 2-2</td>
<td className="px-3 py-2 text-sm whitespace-nowrap"> 2-3</td>
@@ -3399,16 +3389,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<div className="overflow-auto">
{groupedLeftData.map((group, groupIdx) => (
<div key={groupIdx} className="mb-4">
<div className="bg-gray-100 px-3 py-2 text-sm font-semibold">
<div className="bg-muted px-3 py-2 text-sm font-semibold">
{group.groupKey} ({group.count})
</div>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted">
<tr>
{columnsToShow.map((col, idx) => (
<th
key={idx}
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap"
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
style={{
width: col.width ? `${col.width}px` : "auto",
minWidth: "80px",
@@ -3419,12 +3409,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</th>
))}
{hasGroupedLeftActions && (
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap" style={{ width: "80px" }}>
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-border bg-white">
{group.items.map((item, idx) => {
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id";
const itemId = item[sourceColumn] || item.id || item.ID || idx;
@@ -3443,7 +3433,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{columnsToShow.map((col, colIdx) => (
<td
key={colIdx}
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
className="px-3 py-2 text-sm whitespace-nowrap text-foreground"
style={{ textAlign: col.align || "left" }}
>
{formatCellValue(
@@ -3463,9 +3453,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleEditClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-gray-200"
className="rounded p-1 transition-colors hover:bg-muted/80"
>
<Pencil className="h-3.5 w-3.5 text-gray-500" />
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
{(componentConfig.leftPanel?.showDelete !== false) && (
@@ -3474,9 +3464,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleDeleteClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-red-100"
className="rounded p-1 transition-colors hover:bg-destructive/10"
>
<Trash2 className="h-3.5 w-3.5 text-red-500" />
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</button>
)}
</div>
@@ -3500,13 +3490,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
);
return (
<div className="overflow-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="sticky top-0 z-10 bg-gray-50">
<table className="min-w-full divide-y divide-border">
<thead className="sticky top-0 z-10 bg-muted">
<tr>
{columnsToShow.map((col, idx) => (
<th
key={idx}
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap"
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
style={{
width: col.width ? `${col.width}px` : "auto",
minWidth: "80px",
@@ -3517,12 +3507,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</th>
))}
{hasLeftTableActions && (
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap" style={{ width: "80px" }}>
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-border bg-white">
{filteredData.map((item, idx) => {
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id";
const itemId = item[sourceColumn] || item.id || item.ID || idx;
@@ -3541,7 +3531,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{columnsToShow.map((col, colIdx) => (
<td
key={colIdx}
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
className="px-3 py-2 text-sm whitespace-nowrap text-foreground"
style={{ textAlign: col.align || "left" }}
>
{formatCellValue(
@@ -3561,9 +3551,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleEditClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-gray-200"
className="rounded p-1 transition-colors hover:bg-muted/80"
>
<Pencil className="h-3.5 w-3.5 text-gray-500" />
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
{(componentConfig.leftPanel?.showDelete !== false) && (
@@ -3572,9 +3562,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleDeleteClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-red-100"
className="rounded p-1 transition-colors hover:bg-destructive/10"
>
<Trash2 className="h-3.5 w-3.5 text-red-500" />
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</button>
)}
</div>
@@ -3727,9 +3717,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{hasChildren ? (
<div className="flex-shrink-0">
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-gray-500" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-gray-500" />
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</div>
) : (
@@ -3754,10 +3744,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleEditClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-gray-200"
className="rounded p-1 transition-colors hover:bg-muted/80"
title="수정"
>
<Pencil className="h-4 w-4 text-gray-600" />
<Pencil className="h-4 w-4 text-muted-foreground" />
</button>
)}
@@ -3768,10 +3758,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleDeleteClick("left", item);
}}
className="rounded p-1 transition-colors hover:bg-red-100"
className="rounded p-1 transition-colors hover:bg-destructive/10"
title="삭제"
>
<Trash2 className="h-4 w-4 text-red-600" />
<Trash2 className="h-4 w-4 text-destructive" />
</button>
)}
@@ -3782,10 +3772,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleItemAddClick(item);
}}
className="rounded p-1 transition-colors hover:bg-gray-200"
className="rounded p-1 transition-colors hover:bg-muted/80"
title="하위 항목 추가"
>
<Plus className="h-4 w-4 text-gray-600" />
<Plus className="h-4 w-4 text-muted-foreground" />
</button>
)}
</div>
@@ -3837,8 +3827,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{/* 우측 패널 */}
<div
style={{ width: `${100 - leftWidth}%`, minWidth: isPreview ? "0" : `${minRightWidth}px`, height: "100%" }}
className="flex flex-shrink-0 flex-col border-l border-border/60 bg-muted/5"
style={{ width: `${100 - leftWidth}%`, minWidth: isDesignMode ? `${minRightWidth}px` : "0", height: "100%" }}
className="flex flex-col border-l border-border/60 bg-muted/5"
>
<Card className="flex flex-col border-0 bg-transparent shadow-none" style={{ height: "100%" }}>
<CardHeader
@@ -3927,7 +3917,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</div>
</div>
)}
<CardContent className="flex-1 overflow-hidden p-4">
<CardContent className="flex-1 overflow-auto p-4">
{/* 추가 탭 컨텐츠 */}
{activeTabIndex > 0 ? (
(() => {
@@ -4207,192 +4197,36 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
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 형식으로 변환
// componentConfig의 주요 속성을 최상위로 펼침 (일반 화면의 overrides 플래트닝과 동일)
const componentData = {
id: comp.id,
!isDesignMode ? (
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
(() => {
const rightComps = componentConfig.rightPanel!.components;
const canvasW = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
const canvasH = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
const compDataList = rightComps.map((c: PanelInlineComponent) => ({
id: c.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 || {},
// 파일 업로드/미디어 등이 component.tableName, component.columnName을 직접 참조하므로 펼침
tableName: comp.componentConfig?.tableName,
columnName: comp.componentConfig?.columnName,
webType: comp.componentConfig?.webType,
inputType: comp.inputType || comp.componentConfig?.inputType,
};
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();
// 패널 컴포넌트 선택 시 탭 내 선택 해제
if (comp.componentType !== "v2-tabs-widget") {
setNestedTabSelectedCompId(undefined);
}
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={cn(
"h-full w-full",
// 탭/분할 패널 같은 컨테이너 컴포넌트는 pointer-events 활성화
(comp.componentType === "v2-tabs-widget" ||
comp.componentType === "tabs-widget" ||
comp.componentType === "v2-split-panel-layout" ||
comp.componentType === "split-panel-layout")
? ""
: "pointer-events-none"
)}>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
// 🆕 중첩된 컴포넌트 업데이트 핸들러 전달
onUpdateComponent={(updatedComp: any) => {
handleNestedComponentUpdate("right", comp.id, updatedComp);
}}
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
console.log("🔍 [SplitPanel-Right] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
// 탭 내 컴포넌트 선택 상태 업데이트
setNestedTabSelectedCompId(compId);
// 부모 분할 패널 정보와 함께 전역 이벤트 발생
const event = new CustomEvent("nested-tab-component-select", {
detail: {
tabsComponentId: comp.id,
tabId,
componentId: compId,
component: tabComp,
parentSplitPanelId: component.id,
parentPanelSide: "right",
},
});
window.dispatchEvent(event);
}}
selectedTabComponentId={nestedTabSelectedCompId}
/>
</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,
}}
>
componentType: c.componentType,
label: c.label,
position: c.position || { x: 0, y: 0 },
size: c.size || { width: 400, height: 300 },
componentConfig: c.componentConfig || {},
style: c.style || {},
tableName: c.componentConfig?.tableName,
columnName: c.componentConfig?.columnName,
webType: c.componentConfig?.webType,
inputType: (c as any).inputType || c.componentConfig?.inputType,
})) as any;
return (
<ResponsiveGridRenderer
components={compDataList}
canvasWidth={canvasW}
canvasHeight={canvasH}
renderComponent={(comp) => (
<DynamicComponentRenderer
component={componentData as any}
component={comp as any}
isDesignMode={false}
isInteractive={true}
formData={customLeftSelectedData}
@@ -4409,19 +4243,171 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
selectedRowsData={localSelectedRowsData}
onSelectedRowsChange={handleLocalSelectedRowsChange}
/>
)}
/>
);
})()
) : (
<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);
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 || {},
tableName: comp.componentConfig?.tableName,
columnName: comp.componentConfig?.columnName,
webType: comp.componentConfig?.webType,
inputType: (comp as any).inputType || comp.componentConfig?.inputType,
};
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();
if (comp.componentType !== "v2-tabs-widget") {
setNestedTabSelectedCompId(undefined);
}
onSelectPanelComponent?.("right", comp.id, comp);
}}
>
<div
className={cn(
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-muted px-1",
isSelectedComp ? "border-primary" : "border-border"
)}
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-muted-foreground/70" />
<span className="max-w-[100px] truncate text-[9px] text-muted-foreground">
{comp.label || comp.componentType}
</span>
</div>
<div className="flex items-center">
<button
className="rounded p-0.5 hover:bg-muted/80"
onClick={(e) => {
e.stopPropagation();
onSelectPanelComponent?.("right", comp.id, comp);
}}
title="설정"
>
<Settings className="h-2.5 w-2.5 text-muted-foreground" />
</button>
<button
className="rounded p-0.5 hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
handleRemovePanelComponent("right", comp.id);
}}
title="삭제"
>
<Trash2 className="h-2.5 w-2.5 text-destructive" />
</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-border",
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
!(isDraggingComp || isResizingComp) && "transition-all"
)}
style={{
width: displayWidth,
height: displayHeight,
}}
>
<div className={cn(
"h-full w-full",
(comp.componentType === "v2-tabs-widget" ||
comp.componentType === "tabs-widget" ||
comp.componentType === "v2-split-panel-layout" ||
comp.componentType === "split-panel-layout")
? ""
: "pointer-events-none"
)}>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={true}
formData={{}}
onUpdateComponent={(updatedComp: any) => {
handleNestedComponentUpdate("right", comp.id, updatedComp);
}}
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
setNestedTabSelectedCompId(compId);
const event = new CustomEvent("nested-tab-component-select", {
detail: {
tabsComponentId: comp.id,
tabId,
componentId: compId,
component: tabComp,
parentSplitPanelId: component.id,
parentPanelSide: "right",
},
});
window.dispatchEvent(event);
}}
selectedTabComponentId={nestedTabSelectedCompId}
/>
</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>
);
}
})}
</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">
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-input bg-muted/50">
<Plus className="mb-2 h-8 w-8 text-muted-foreground/70" />
<p className="text-sm font-medium text-muted-foreground">
</p>
<p className="mt-1 text-xs text-gray-400">
<p className="mt-1 text-xs text-muted-foreground/70">
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
</p>
</div>
@@ -4508,10 +4494,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}));
}
const tableMinWidth = columnsToShow.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
return (
<div className="flex h-full w-full flex-col">
<div className="min-h-0 flex-1 overflow-auto">
<table className="min-w-full">
<table style={{ minWidth: `${tableMinWidth}px` }}>
<thead className="sticky top-0 z-10">
<tr className="border-b-2 border-border/60">
{columnsToShow.map((col, idx) => (
@@ -4587,10 +4574,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
e.stopPropagation();
handleDeleteClick("right", item);
}}
className="rounded p-1 transition-colors hover:bg-red-100"
className="rounded p-1 transition-colors hover:bg-destructive/10"
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
>
<Trash2 className="h-4 w-4 text-red-600" />
<Trash2 className="h-4 w-4 text-destructive" />
</button>
)}
</div>
@@ -4636,10 +4623,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
const hasActions = hasEditButton || hasDeleteButton;
const tableMinW2 = columnsToDisplay.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
return filteredData.length > 0 ? (
<div className="flex h-full w-full flex-col">
<div className="min-h-0 flex-1 overflow-auto">
<table className="w-full text-sm">
<table className="text-sm" style={{ minWidth: `${tableMinW2}px` }}>
<thead className="sticky top-0 z-10 bg-background">
<tr className="border-b-2 border-border/60">
{columnsToDisplay.map((col) => (
@@ -4919,7 +4907,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<div key={index}>
<Label htmlFor={col.name} className="text-xs sm:text-sm">
{col.label} {col.required && <span className="text-destructive">*</span>}
{isPreFilled && <span className="ml-2 text-[10px] text-blue-600">( )</span>}
{isPreFilled && <span className="ml-2 text-[10px] text-primary">( )</span>}
</Label>
<Input
id={col.name}