"use client"; import React, { createContext, useContext, useState, useCallback, useRef, useMemo } from "react"; /** * SplitPanelResize Context 타입 정의 * 분할 패널의 드래그 리사이즈 상태를 외부 컴포넌트(버튼 등)와 공유하기 위한 Context * * 주의: contexts/SplitPanelContext.tsx는 데이터 전달용 Context이고, * 이 Context는 드래그 리사이즈 시 버튼 위치 조정을 위한 별도 Context입니다. */ /** * 분할 패널 정보 (컴포넌트 좌표 기준) */ export interface SplitPanelInfo { id: string; // 분할 패널의 좌표 (스크린 캔버스 기준, px) x: number; y: number; width: number; height: number; // 좌측 패널 비율 (0-100) leftWidthPercent: number; // 초기 좌측 패널 비율 (드래그 시작 시점) initialLeftWidthPercent: number; // 드래그 중 여부 isDragging: boolean; } export interface SplitPanelResizeContextValue { // 등록된 분할 패널들 splitPanels: Map; // 분할 패널 등록/해제/업데이트 registerSplitPanel: (id: string, info: Omit) => void; unregisterSplitPanel: (id: string) => void; updateSplitPanel: (id: string, updates: Partial) => void; // 컴포넌트가 어떤 분할 패널의 좌측 영역 위에 있는지 확인 // 반환값: { panelId, offsetX } 또는 null getOverlappingSplitPanel: ( componentX: number, componentY: number, componentWidth: number, componentHeight: number, ) => { panelId: string; panel: SplitPanelInfo; isInLeftPanel: boolean } | null; // 컴포넌트의 조정된 X 좌표 계산 // 분할 패널 좌측 영역 위에 있으면, 드래그에 따라 조정된 X 좌표 반환 getAdjustedX: (componentX: number, componentY: number, componentWidth: number, componentHeight: number) => number; // 레거시 호환 (단일 분할 패널용) leftWidthPercent: number; containerRect: DOMRect | null; dividerX: number; isDragging: boolean; splitPanelId: string | null; updateLeftWidth: (percent: number) => void; updateContainerRect: (rect: DOMRect | null) => void; updateDragging: (dragging: boolean) => void; } // Context 생성 const SplitPanelResizeContext = createContext(null); /** * SplitPanelResize Context Provider * 스크린 빌더 레벨에서 감싸서 사용 */ export const SplitPanelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { // 등록된 분할 패널들 const splitPanelsRef = useRef>(new Map()); const [, forceUpdate] = useState(0); // 레거시 호환용 상태 const [legacyLeftWidthPercent, setLegacyLeftWidthPercent] = useState(30); const [legacyContainerRect, setLegacyContainerRect] = useState(null); const [legacyIsDragging, setLegacyIsDragging] = useState(false); const [legacySplitPanelId, setLegacySplitPanelId] = useState(null); // 분할 패널 등록 const registerSplitPanel = useCallback((id: string, info: Omit) => { splitPanelsRef.current.set(id, { id, ...info }); setLegacySplitPanelId(id); setLegacyLeftWidthPercent(info.leftWidthPercent); forceUpdate((n) => n + 1); }, []); // 분할 패널 해제 const unregisterSplitPanel = useCallback( (id: string) => { splitPanelsRef.current.delete(id); if (legacySplitPanelId === id) { setLegacySplitPanelId(null); } forceUpdate((n) => n + 1); }, [legacySplitPanelId], ); // 분할 패널 업데이트 const updateSplitPanel = useCallback((id: string, updates: Partial) => { const panel = splitPanelsRef.current.get(id); if (panel) { const updatedPanel = { ...panel, ...updates }; splitPanelsRef.current.set(id, updatedPanel); // 레거시 호환 상태 업데이트 if (updates.leftWidthPercent !== undefined) { setLegacyLeftWidthPercent(updates.leftWidthPercent); } if (updates.isDragging !== undefined) { setLegacyIsDragging(updates.isDragging); } forceUpdate((n) => n + 1); } }, []); /** * 컴포넌트가 어떤 분할 패널의 좌측 영역 위에 있는지 확인 */ const getOverlappingSplitPanel = useCallback( ( componentX: number, componentY: number, componentWidth: number, componentHeight: number, ): { panelId: string; panel: SplitPanelInfo; isInLeftPanel: boolean } | null => { for (const [panelId, panel] of splitPanelsRef.current) { // 컴포넌트의 중심점 const componentCenterX = componentX + componentWidth / 2; const componentCenterY = componentY + componentHeight / 2; // 컴포넌트가 분할 패널 영역 내에 있는지 확인 const isInPanelX = componentCenterX >= panel.x && componentCenterX <= panel.x + panel.width; const isInPanelY = componentCenterY >= panel.y && componentCenterY <= panel.y + panel.height; if (isInPanelX && isInPanelY) { // 좌측 패널의 현재 너비 (px) const leftPanelWidth = (panel.width * panel.leftWidthPercent) / 100; // 좌측 패널 경계 (분할 패널 기준 상대 좌표) const dividerX = panel.x + leftPanelWidth; // 컴포넌트 중심이 좌측 패널 내에 있는지 확인 const isInLeftPanel = componentCenterX < dividerX; return { panelId, panel, isInLeftPanel }; } } return null; }, [], ); /** * 컴포넌트의 조정된 X 좌표 계산 * 분할 패널 좌측 영역 위에 있으면, 드래그에 따라 조정된 X 좌표 반환 * * 핵심 로직: * - 버튼의 원래 X 좌표가 초기 좌측 패널 너비 내에서 어느 비율에 있는지 계산 * - 드래그로 좌측 패널 너비가 바뀌면, 같은 비율을 유지하도록 X 좌표 조정 */ const getAdjustedX = useCallback( (componentX: number, componentY: number, componentWidth: number, componentHeight: number): number => { const overlap = getOverlappingSplitPanel(componentX, componentY, componentWidth, componentHeight); if (!overlap || !overlap.isInLeftPanel) { // 분할 패널 위에 없거나, 우측 패널 위에 있으면 원래 위치 유지 return componentX; } const { panel } = overlap; // 초기 좌측 패널 너비 (설정된 splitRatio 기준) const initialLeftPanelWidth = (panel.width * panel.initialLeftWidthPercent) / 100; // 현재 좌측 패널 너비 (드래그로 변경된 값) const currentLeftPanelWidth = (panel.width * panel.leftWidthPercent) / 100; // 변화가 없으면 원래 위치 반환 if (Math.abs(initialLeftPanelWidth - currentLeftPanelWidth) < 1) { return componentX; } // 컴포넌트의 분할 패널 내 상대 X 좌표 const relativeX = componentX - panel.x; // 좌측 패널 내에서의 비율 (0~1) const ratioInLeftPanel = relativeX / initialLeftPanelWidth; // 조정된 상대 X 좌표 = 원래 비율 * 현재 좌측 패널 너비 const adjustedRelativeX = ratioInLeftPanel * currentLeftPanelWidth; // 절대 X 좌표로 변환 const adjustedX = panel.x + adjustedRelativeX; console.log("📍 [SplitPanel] 버튼 위치 조정:", { componentX, panelX: panel.x, relativeX, initialLeftPanelWidth, currentLeftPanelWidth, ratioInLeftPanel, adjustedX, delta: adjustedX - componentX, }); return adjustedX; }, [getOverlappingSplitPanel], ); // 레거시 호환 - dividerX 계산 const legacyDividerX = legacyContainerRect ? (legacyContainerRect.width * legacyLeftWidthPercent) / 100 : 0; // 레거시 호환 함수들 const updateLeftWidth = useCallback((percent: number) => { setLegacyLeftWidthPercent(percent); // 첫 번째 분할 패널 업데이트 const firstPanelId = splitPanelsRef.current.keys().next().value; if (firstPanelId) { const panel = splitPanelsRef.current.get(firstPanelId); if (panel) { splitPanelsRef.current.set(firstPanelId, { ...panel, leftWidthPercent: percent }); } } forceUpdate((n) => n + 1); }, []); const updateContainerRect = useCallback((rect: DOMRect | null) => { setLegacyContainerRect(rect); }, []); const updateDragging = useCallback((dragging: boolean) => { setLegacyIsDragging(dragging); // 첫 번째 분할 패널 업데이트 const firstPanelId = splitPanelsRef.current.keys().next().value; if (firstPanelId) { const panel = splitPanelsRef.current.get(firstPanelId); if (panel) { // 드래그 시작 시 초기 비율 저장 const updates: Partial = { isDragging: dragging }; if (dragging) { updates.initialLeftWidthPercent = panel.leftWidthPercent; } splitPanelsRef.current.set(firstPanelId, { ...panel, ...updates }); } } forceUpdate((n) => n + 1); }, []); const value = useMemo( () => ({ splitPanels: splitPanelsRef.current, registerSplitPanel, unregisterSplitPanel, updateSplitPanel, getOverlappingSplitPanel, getAdjustedX, // 레거시 호환 leftWidthPercent: legacyLeftWidthPercent, containerRect: legacyContainerRect, dividerX: legacyDividerX, isDragging: legacyIsDragging, splitPanelId: legacySplitPanelId, updateLeftWidth, updateContainerRect, updateDragging, }), [ registerSplitPanel, unregisterSplitPanel, updateSplitPanel, getOverlappingSplitPanel, getAdjustedX, legacyLeftWidthPercent, legacyContainerRect, legacyDividerX, legacyIsDragging, legacySplitPanelId, updateLeftWidth, updateContainerRect, updateDragging, ], ); return {children}; }; /** * SplitPanelResize Context 사용 훅 * 분할 패널의 드래그 리사이즈 상태를 구독합니다. */ export const useSplitPanel = (): SplitPanelResizeContextValue => { const context = useContext(SplitPanelResizeContext); // Context가 없으면 기본값 반환 (Provider 외부에서 사용 시) if (!context) { return { splitPanels: new Map(), registerSplitPanel: () => {}, unregisterSplitPanel: () => {}, updateSplitPanel: () => {}, getOverlappingSplitPanel: () => null, getAdjustedX: (x) => x, leftWidthPercent: 30, containerRect: null, dividerX: 0, isDragging: false, splitPanelId: null, updateLeftWidth: () => {}, updateContainerRect: () => {}, updateDragging: () => {}, }; } return context; }; /** * 컴포넌트의 조정된 위치를 계산하는 훅 * 분할 패널 좌측 영역 위에 있으면, 드래그에 따라 X 좌표가 조정됨 * * @param componentX - 컴포넌트의 X 좌표 (px) * @param componentY - 컴포넌트의 Y 좌표 (px) * @param componentWidth - 컴포넌트 너비 (px) * @param componentHeight - 컴포넌트 높이 (px) * @returns 조정된 X 좌표와 관련 정보 */ export const useAdjustedComponentPosition = ( componentX: number, componentY: number, componentWidth: number, componentHeight: number, ) => { const context = useSplitPanel(); const adjustedX = context.getAdjustedX(componentX, componentY, componentWidth, componentHeight); const overlap = context.getOverlappingSplitPanel(componentX, componentY, componentWidth, componentHeight); return { adjustedX, isInSplitPanel: !!overlap, isInLeftPanel: overlap?.isInLeftPanel ?? false, isDragging: overlap?.panel.isDragging ?? false, panelId: overlap?.panelId ?? null, }; }; /** * 버튼 등 외부 컴포넌트에서 분할 패널 좌측 영역 내 위치를 계산하는 훅 (레거시 호환) */ export const useAdjustedPosition = (originalXPercent: number) => { const { leftWidthPercent, containerRect, dividerX, isDragging } = useSplitPanel(); const isInLeftPanel = originalXPercent <= leftWidthPercent; const adjustedXPercent = isInLeftPanel ? (originalXPercent / 100) * leftWidthPercent : originalXPercent; const adjustedXPx = containerRect ? (containerRect.width * adjustedXPercent) / 100 : 0; return { adjustedXPercent, adjustedXPx, isInLeftPanel, isDragging, dividerX, containerRect, leftWidthPercent, }; }; /** * 버튼이 좌측 패널 위에 배치되었을 때, 드래그에 따라 위치가 조정되는 스타일을 반환하는 훅 (레거시 호환) */ export const useSplitPanelAwarePosition = ( initialLeftPercent: number, options?: { followDivider?: boolean; offset?: number; }, ) => { const { leftWidthPercent, containerRect, dividerX, isDragging } = useSplitPanel(); const { followDivider = false, offset = 0 } = options || {}; if (followDivider) { return { left: containerRect ? `${dividerX + offset}px` : `${leftWidthPercent}%`, transition: isDragging ? "none" : "left 0.15s ease-out", }; } const adjustedLeft = (initialLeftPercent / 100) * leftWidthPercent; return { left: `${adjustedLeft}%`, transition: isDragging ? "none" : "left 0.15s ease-out", }; }; export default SplitPanelResizeContext;