205 lines
6.7 KiB
TypeScript
205 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback } from "react";
|
|
import { BaseLayoutRenderer, LayoutRendererProps } from "./BaseLayoutRenderer";
|
|
|
|
export default class SplitLayoutRenderer extends BaseLayoutRenderer {
|
|
render(): React.ReactElement {
|
|
const { layout, isDesignMode, isSelected, onClick, className } = this.props;
|
|
|
|
if (!layout.layoutConfig.split) {
|
|
return <div className="error-layout">분할 레이아웃 설정이 없습니다.</div>;
|
|
}
|
|
|
|
return (
|
|
<SplitLayoutComponent
|
|
layout={layout}
|
|
isDesignMode={isDesignMode}
|
|
isSelected={isSelected}
|
|
onClick={onClick}
|
|
className={className}
|
|
renderer={this}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
interface SplitLayoutComponentProps {
|
|
layout: any;
|
|
isDesignMode?: boolean;
|
|
isSelected?: boolean;
|
|
onClick?: (e: React.MouseEvent) => void;
|
|
className?: string;
|
|
renderer: SplitLayoutRenderer;
|
|
}
|
|
|
|
const SplitLayoutComponent: React.FC<SplitLayoutComponentProps> = ({
|
|
layout,
|
|
isDesignMode,
|
|
isSelected,
|
|
onClick,
|
|
className,
|
|
renderer,
|
|
}) => {
|
|
const splitConfig = layout.layoutConfig.split;
|
|
const [sizes, setSizes] = useState(splitConfig.ratio || [50, 50]);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const containerStyle = renderer.getLayoutContainerStyle();
|
|
|
|
// 분할 컨테이너 스타일
|
|
const splitStyle: React.CSSProperties = {
|
|
...containerStyle,
|
|
display: "flex",
|
|
flexDirection: splitConfig.direction === "horizontal" ? "row" : "column",
|
|
overflow: "hidden",
|
|
};
|
|
|
|
// 디자인 모드 스타일
|
|
if (isDesignMode) {
|
|
splitStyle.border = isSelected ? "2px solid #3b82f6" : "1px solid #e2e8f0";
|
|
splitStyle.borderRadius = "8px";
|
|
}
|
|
|
|
// 스플리터 드래그 핸들러
|
|
const handleSplitterDrag = useCallback(
|
|
(e: React.MouseEvent, index: number) => {
|
|
if (!splitConfig.resizable || !isDesignMode) return;
|
|
|
|
setIsDragging(true);
|
|
const startPos = splitConfig.direction === "horizontal" ? e.clientX : e.clientY;
|
|
const startSizes = [...sizes];
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
const currentPos = splitConfig.direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY;
|
|
const delta = currentPos - startPos;
|
|
const containerSize =
|
|
splitConfig.direction === "horizontal"
|
|
? (e.currentTarget as HTMLElement).parentElement!.clientWidth
|
|
: (e.currentTarget as HTMLElement).parentElement!.clientHeight;
|
|
|
|
const deltaPercent = (delta / containerSize) * 100;
|
|
|
|
const newSizes = [...startSizes];
|
|
newSizes[index] = Math.max(splitConfig.minSize?.[index] || 10, Math.min(90, startSizes[index] + deltaPercent));
|
|
newSizes[index + 1] = Math.max(
|
|
splitConfig.minSize?.[index + 1] || 10,
|
|
Math.min(90, startSizes[index + 1] - deltaPercent),
|
|
);
|
|
|
|
setSizes(newSizes);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
},
|
|
[splitConfig, sizes, isDesignMode],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={`split-layout ${isDesignMode ? "design-mode" : ""} ${className || ""}`}
|
|
style={splitStyle}
|
|
onClick={onClick}
|
|
draggable={isDesignMode && !isDragging}
|
|
onDragStart={(e) => !isDragging && renderer.props.onDragStart?.(e)}
|
|
onDragEnd={(e) => !isDragging && renderer.props.onDragEnd?.(e)}
|
|
>
|
|
{layout.zones.map((zone: any, index: number) => {
|
|
const zoneChildren = renderer.getZoneChildren(zone.id);
|
|
const isHorizontal = splitConfig.direction === "horizontal";
|
|
|
|
// 패널 크기 계산
|
|
const panelSize = sizes[index] || 100 / layout.zones.length;
|
|
const panelStyle: React.CSSProperties = {
|
|
[isHorizontal ? "width" : "height"]: `${panelSize}%`,
|
|
[isHorizontal ? "height" : "width"]: "100%",
|
|
overflow: "auto",
|
|
};
|
|
|
|
return (
|
|
<React.Fragment key={zone.id}>
|
|
{/* 패널 */}
|
|
{renderer.renderZone(zone, zoneChildren, {
|
|
style: panelStyle,
|
|
className: "split-panel",
|
|
})}
|
|
|
|
{/* 스플리터 (마지막 패널 제외) */}
|
|
{index < layout.zones.length - 1 && (
|
|
<div
|
|
className={`splitter ${isHorizontal ? "horizontal" : "vertical"} ${isDragging ? "dragging" : ""}`}
|
|
style={{
|
|
[isHorizontal ? "width" : "height"]: `${splitConfig.splitterSize || 4}px`,
|
|
[isHorizontal ? "height" : "width"]: "100%",
|
|
backgroundColor: "#e2e8f0",
|
|
cursor: isHorizontal ? "col-resize" : "row-resize",
|
|
position: "relative",
|
|
zIndex: 10,
|
|
...(isDragging && {
|
|
backgroundColor: "#3b82f6",
|
|
}),
|
|
}}
|
|
onMouseDown={(e) => handleSplitterDrag(e, index)}
|
|
>
|
|
{/* 스플리터 핸들 */}
|
|
<div
|
|
className="splitter-handle"
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
[isHorizontal ? "width" : "height"]: "20px",
|
|
[isHorizontal ? "height" : "width"]: "4px",
|
|
backgroundColor: "#94a3b8",
|
|
borderRadius: "2px",
|
|
opacity: splitConfig.resizable && isDesignMode ? 1 : 0,
|
|
transition: "opacity 0.2s ease",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
|
|
{/* 디자인 모드에서 존이 없을 때 안내 메시지 */}
|
|
{isDesignMode && layout.zones.length === 0 && (
|
|
<div
|
|
className="empty-split-container"
|
|
style={{
|
|
flex: 1,
|
|
border: "2px dashed #cbd5e1",
|
|
borderRadius: "8px",
|
|
backgroundColor: "rgba(148, 163, 184, 0.05)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "14px",
|
|
color: "#64748b",
|
|
minHeight: "100px",
|
|
padding: "20px",
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
분할 레이아웃에 존을 추가하세요
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// React 컴포넌트로 래핑
|
|
export const SplitLayout: React.FC<LayoutRendererProps> = (props) => {
|
|
const renderer = new SplitLayoutRenderer(props);
|
|
return renderer.render();
|
|
};
|
|
|