Merge branch 'gbpark-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user