"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 = ({ 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(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 = Math.max(50, cw * 0.15); const MAX_POS = cw - Math.max(50, cw * 0.15); 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 (
스플릿선
); } return (
); }; export const SplitLineWrapper: React.FC = (props) => { return ; };