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