ERP-node/frontend/lib/registry/components/v2-split-line/SplitLineComponent.tsx

276 lines
8.5 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useCallback, useEffect, useRef } from "react";
import { ComponentRendererProps } from "@/types/component";
import { SplitLineConfig } from "./types";
import { setCanvasSplit, resetCanvasSplit } from "./canvasSplitStore";
export interface SplitLineComponentProps extends ComponentRendererProps {
config?: SplitLineConfig;
}
export const SplitLineComponent: React.FC<SplitLineComponentProps> = ({
component,
isDesignMode = false,
isSelected = false,
onClick,
config,
className,
style,
...props
}) => {
const componentConfig = {
...config,
...component.componentConfig,
} as SplitLineConfig;
const resizable = componentConfig.resizable ?? true;
const lineColor = componentConfig.lineColor || "#e2e8f0";
const lineWidth = componentConfig.lineWidth || 4;
const [dragOffset, setDragOffset] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// CSS transform: scale()이 적용된 캔버스에서 정확한 디자인 해상도
const detectCanvasWidth = useCallback((): number => {
if (containerRef.current) {
const canvas =
containerRef.current.closest("[data-screen-runtime]") ||
containerRef.current.closest("[data-screen-canvas]");
if (canvas) {
const w = parseInt((canvas as HTMLElement).style.width);
if (w > 0) return w;
}
}
const canvas = document.querySelector("[data-screen-runtime]");
if (canvas) {
const w = parseInt((canvas as HTMLElement).style.width);
if (w > 0) return w;
}
return 1200;
}, []);
// CSS scale 보정 계수
const getScaleFactor = useCallback((): number => {
if (containerRef.current) {
const canvas = containerRef.current.closest("[data-screen-runtime]");
if (canvas) {
const el = canvas as HTMLElement;
const designWidth = parseInt(el.style.width) || 1200;
const renderedWidth = el.getBoundingClientRect().width;
if (renderedWidth > 0 && designWidth > 0) {
return designWidth / renderedWidth;
}
}
}
return 1;
}, []);
// 스코프 ID (같은 data-screen-runtime 안의 컴포넌트만 영향)
const scopeIdRef = useRef("");
// 글로벌 스토어에 등록 (런타임 모드)
useEffect(() => {
if (isDesignMode) return;
const timer = setTimeout(() => {
const cw = detectCanvasWidth();
const posX = component.position?.x || 0;
// 스코프 ID: 가장 가까운 data-screen-runtime 요소에 고유 ID 부여
let scopeId = "";
if (containerRef.current) {
const runtimeEl = containerRef.current.closest("[data-screen-runtime]");
if (runtimeEl) {
scopeId = runtimeEl.getAttribute("data-split-scope") || "";
if (!scopeId) {
scopeId = `split-scope-${component.id}`;
runtimeEl.setAttribute("data-split-scope", scopeId);
}
}
}
scopeIdRef.current = scopeId;
console.log("[SplitLine] 등록:", { canvasWidth: cw, positionX: posX, scopeId });
setCanvasSplit({
initialDividerX: posX,
currentDividerX: posX,
canvasWidth: cw,
isDragging: false,
active: true,
scopeId,
});
}, 100);
return () => {
clearTimeout(timer);
resetCanvasSplit();
};
}, [isDesignMode, component.id, component.position?.x, detectCanvasWidth]);
// 드래그 핸들러 (requestAnimationFrame으로 스로틀링 → 렉 방지)
const rafIdRef = useRef(0);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
if (!resizable || isDesignMode) return;
e.preventDefault();
e.stopPropagation();
const posX = component.position?.x || 0;
const startX = e.clientX;
const startOffset = dragOffset;
const scaleFactor = getScaleFactor();
const cw = detectCanvasWidth();
const MIN_POS = 50;
const MAX_POS = cw - 50;
setIsDragging(true);
setCanvasSplit({ isDragging: true });
const handleMouseMove = (moveEvent: MouseEvent) => {
// rAF로 스로틀링: 프레임당 1회만 업데이트
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = requestAnimationFrame(() => {
const rawDelta = moveEvent.clientX - startX;
const delta = rawDelta * scaleFactor;
let newOffset = startOffset + delta;
const newDividerX = posX + newOffset;
if (newDividerX < MIN_POS) newOffset = MIN_POS - posX;
if (newDividerX > MAX_POS) newOffset = MAX_POS - posX;
setDragOffset(newOffset);
setCanvasSplit({ currentDividerX: posX + newOffset });
});
};
const handleMouseUp = () => {
cancelAnimationFrame(rafIdRef.current);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.userSelect = "";
document.body.style.cursor = "";
setIsDragging(false);
setCanvasSplit({ isDragging: false });
};
document.body.style.userSelect = "none";
document.body.style.cursor = "col-resize";
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[resizable, isDesignMode, dragOffset, component.position?.x, getScaleFactor, detectCanvasWidth],
);
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.();
};
// props 필터링
const {
selectedScreen: _1, onZoneComponentDrop: _2, onZoneClick: _3,
componentConfig: _4, component: _5, isSelected: _6,
onClick: _7, onDragStart: _8, onDragEnd: _9,
size: _10, position: _11, style: _12,
screenId: _13, tableName: _14, onRefresh: _15, onClose: _16,
webType: _17, autoGeneration: _18, isInteractive: _19,
formData: _20, onFormDataChange: _21,
menuId: _22, menuObjid: _23, onSave: _24,
userId: _25, userName: _26, companyCode: _27,
isInModal: _28, readonly: _29, originalData: _30,
_originalData: _31, _initialData: _32, _groupedData: _33,
allComponents: _34, onUpdateLayout: _35,
selectedRows: _36, selectedRowsData: _37, onSelectedRowsChange: _38,
sortBy: _39, sortOrder: _40, tableDisplayData: _41,
flowSelectedData: _42, flowSelectedStepId: _43, onFlowSelectedDataChange: _44,
onConfigChange: _45, refreshKey: _46, flowRefreshKey: _47, onFlowRefresh: _48,
isPreview: _49, groupedData: _50,
...domProps
} = props as any;
if (isDesignMode) {
return (
<div
ref={containerRef}
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
position: "relative",
...style,
}}
className={className}
onClick={handleClick}
{...domProps}
>
<div
style={{
width: `${lineWidth}px`,
height: "100%",
borderLeft: `${lineWidth}px dashed ${isSelected ? "#3b82f6" : lineColor}`,
}}
/>
<div
style={{
position: "absolute",
top: "4px",
left: "50%",
transform: "translateX(-50%)",
fontSize: "10px",
color: isSelected ? "#3b82f6" : "#9ca3af",
whiteSpace: "nowrap",
backgroundColor: "rgba(255,255,255,0.9)",
padding: "1px 6px",
borderRadius: "4px",
fontWeight: 500,
}}
>
릿
</div>
</div>
);
}
return (
<div
ref={containerRef}
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: resizable ? "col-resize" : "default",
transform: `translateX(${dragOffset}px)`,
transition: isDragging ? "none" : "transform 0.1s ease-out",
zIndex: 50,
...style,
}}
className={className}
onMouseDown={handleMouseDown}
{...domProps}
>
<div
style={{
width: `${lineWidth}px`,
height: "100%",
backgroundColor: isDragging ? "hsl(var(--primary))" : lineColor,
transition: isDragging ? "none" : "background-color 0.15s ease",
}}
className="hover:bg-primary"
/>
</div>
);
};
export const SplitLineWrapper: React.FC<SplitLineComponentProps> = (props) => {
return <SplitLineComponent {...props} />;
};