Compare commits
No commits in common. "e8af7ae4c6ed5ff632d236668a7073ebee208326" and "88024b4e606bd30a89c7c5dcc538d0d1a19d2d98" have entirely different histories.
e8af7ae4c6
...
88024b4e60
|
|
@ -21,7 +21,6 @@ import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 🆕 테이블 옵션
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 🆕 테이블 옵션
|
||||||
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 🆕 높이 관리
|
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 🆕 높이 관리
|
||||||
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 🆕 컴포넌트 간 통신
|
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 🆕 컴포넌트 간 통신
|
||||||
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 🆕 분할 패널 리사이즈
|
|
||||||
|
|
||||||
function ScreenViewPage() {
|
function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -308,7 +307,10 @@ function ScreenViewPage() {
|
||||||
return (
|
return (
|
||||||
<ScreenPreviewProvider isPreviewMode={false}>
|
<ScreenPreviewProvider isPreviewMode={false}>
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div ref={containerRef} className="bg-background h-full w-full overflow-auto p-3">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="bg-background h-full w-full overflow-auto p-3"
|
||||||
|
>
|
||||||
{/* 레이아웃 준비 중 로딩 표시 */}
|
{/* 레이아웃 준비 중 로딩 표시 */}
|
||||||
{!layoutReady && (
|
{!layoutReady && (
|
||||||
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">
|
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">
|
||||||
|
|
@ -356,6 +358,7 @@ function ScreenViewPage() {
|
||||||
return isButton;
|
return isButton;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
topLevelComponents.forEach((component) => {
|
topLevelComponents.forEach((component) => {
|
||||||
const isButton =
|
const isButton =
|
||||||
(component.type === "component" &&
|
(component.type === "component" &&
|
||||||
|
|
@ -796,9 +799,7 @@ function ScreenViewPageWrapper() {
|
||||||
return (
|
return (
|
||||||
<TableSearchWidgetHeightProvider>
|
<TableSearchWidgetHeightProvider>
|
||||||
<ScreenContextProvider>
|
<ScreenContextProvider>
|
||||||
<SplitPanelProvider>
|
|
||||||
<ScreenViewPage />
|
<ScreenViewPage />
|
||||||
</SplitPanelProvider>
|
|
||||||
</ScreenContextProvider>
|
</ScreenContextProvider>
|
||||||
</TableSearchWidgetHeightProvider>
|
</TableSearchWidgetHeightProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ import { cn } from "@/lib/utils";
|
||||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
|
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
|
||||||
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔗 연쇄 드롭다운 래퍼 컴포넌트
|
* 🔗 연쇄 드롭다운 래퍼 컴포넌트
|
||||||
|
|
@ -2102,7 +2101,6 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
: component;
|
: component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitPanelProvider>
|
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 테이블 옵션 툴바 */}
|
{/* 테이블 옵션 툴바 */}
|
||||||
|
|
@ -2211,6 +2209,5 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</TableOptionsProvider>
|
</TableOptionsProvider>
|
||||||
</SplitPanelProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { ComponentData, WebType, isWidgetComponent, isContainerComponent } from "@/types";
|
import { ComponentData, WebType, isWidgetComponent, isContainerComponent } from "@/types";
|
||||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -14,7 +14,6 @@ import { FileUpload } from "./widgets/FileUpload";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
|
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
|
||||||
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
|
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
|
||||||
import { useSplitPanel } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
Type,
|
Type,
|
||||||
|
|
@ -254,8 +253,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
|
|
||||||
// 플로우 위젯의 실제 높이 측정
|
// 플로우 위젯의 실제 높이 측정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isFlowWidget =
|
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||||
type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
|
||||||
|
|
||||||
if (isFlowWidget && contentRef.current) {
|
if (isFlowWidget && contentRef.current) {
|
||||||
const measureHeight = () => {
|
const measureHeight = () => {
|
||||||
|
|
@ -402,118 +400,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
}, [component.id, fileUpdateTrigger]);
|
}, [component.id, fileUpdateTrigger]);
|
||||||
|
|
||||||
// 컴포넌트 스타일 계산
|
// 컴포넌트 스타일 계산
|
||||||
const isFlowWidget =
|
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||||
type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
|
||||||
const isSectionPaper = type === "component" && (component as any).componentConfig?.type === "section-paper";
|
const isSectionPaper = type === "component" && (component as any).componentConfig?.type === "section-paper";
|
||||||
|
|
||||||
const positionX = position?.x || 0;
|
const positionX = position?.x || 0;
|
||||||
const positionY = position?.y || 0;
|
const positionY = position?.y || 0;
|
||||||
|
|
||||||
// 🆕 분할 패널 리사이즈 Context
|
|
||||||
const { getAdjustedX, getOverlappingSplitPanel } = useSplitPanel();
|
|
||||||
|
|
||||||
// 버튼 컴포넌트인지 확인 (분할 패널 위치 조정 대상)
|
|
||||||
const componentType = (component as any).componentType || "";
|
|
||||||
const componentId = (component as any).componentId || "";
|
|
||||||
const widgetType = (component as any).widgetType || "";
|
|
||||||
|
|
||||||
const isButtonComponent =
|
|
||||||
(type === "widget" && widgetType === "button") ||
|
|
||||||
(type === "component" &&
|
|
||||||
(["button-primary", "button-secondary"].includes(componentType) ||
|
|
||||||
["button-primary", "button-secondary"].includes(componentId)));
|
|
||||||
|
|
||||||
// 디버깅: 모든 컴포넌트의 타입 정보 출력 (버튼 관련만)
|
|
||||||
if (componentType.includes("button") || componentId.includes("button") || widgetType.includes("button")) {
|
|
||||||
console.log("🔘 [RealtimePreview] 버튼 컴포넌트 발견:", {
|
|
||||||
id: component.id,
|
|
||||||
type,
|
|
||||||
componentType,
|
|
||||||
componentId,
|
|
||||||
widgetType,
|
|
||||||
isButtonComponent,
|
|
||||||
positionX,
|
|
||||||
positionY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 분할 패널 위 버튼 위치 자동 조정
|
|
||||||
const { adjustedPositionX, isOnSplitPanel, isDraggingSplitPanel } = useMemo(() => {
|
|
||||||
// 버튼이 아니거나 분할 패널 컴포넌트 자체인 경우 조정하지 않음
|
|
||||||
const isSplitPanelComponent =
|
|
||||||
type === "component" &&
|
|
||||||
["split-panel-layout", "split-panel-layout2"].includes((component as any).componentType || "");
|
|
||||||
|
|
||||||
if (!isButtonComponent || isSplitPanelComponent) {
|
|
||||||
return { adjustedPositionX: positionX, isOnSplitPanel: false, isDraggingSplitPanel: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentWidth = size?.width || 100;
|
|
||||||
const componentHeight = size?.height || 40;
|
|
||||||
|
|
||||||
// 분할 패널 위에 있는지 확인
|
|
||||||
const overlap = getOverlappingSplitPanel(positionX, positionY, componentWidth, componentHeight);
|
|
||||||
|
|
||||||
// 디버깅: 버튼이 분할 패널 위에 있는지 확인
|
|
||||||
if (isButtonComponent) {
|
|
||||||
console.log("🔍 [RealtimePreview] 버튼 분할 패널 감지:", {
|
|
||||||
componentId: component.id,
|
|
||||||
componentType: (component as any).componentType,
|
|
||||||
positionX,
|
|
||||||
positionY,
|
|
||||||
componentWidth,
|
|
||||||
componentHeight,
|
|
||||||
hasOverlap: !!overlap,
|
|
||||||
isInLeftPanel: overlap?.isInLeftPanel,
|
|
||||||
panelInfo: overlap
|
|
||||||
? {
|
|
||||||
panelId: overlap.panelId,
|
|
||||||
panelX: overlap.panel.x,
|
|
||||||
panelY: overlap.panel.y,
|
|
||||||
panelWidth: overlap.panel.width,
|
|
||||||
leftWidthPercent: overlap.panel.leftWidthPercent,
|
|
||||||
initialLeftWidthPercent: overlap.panel.initialLeftWidthPercent,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overlap || !overlap.isInLeftPanel) {
|
|
||||||
// 분할 패널 위에 없거나 우측 패널 위에 있음
|
|
||||||
return {
|
|
||||||
adjustedPositionX: positionX,
|
|
||||||
isOnSplitPanel: !!overlap,
|
|
||||||
isDraggingSplitPanel: overlap?.panel.isDragging ?? false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 좌측 패널 위에 있음 - 위치 조정
|
|
||||||
const adjusted = getAdjustedX(positionX, positionY, componentWidth, componentHeight);
|
|
||||||
|
|
||||||
console.log("✅ [RealtimePreview] 버튼 위치 조정 적용:", {
|
|
||||||
componentId: component.id,
|
|
||||||
originalX: positionX,
|
|
||||||
adjustedX: adjusted,
|
|
||||||
delta: adjusted - positionX,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
adjustedPositionX: adjusted,
|
|
||||||
isOnSplitPanel: true,
|
|
||||||
isDraggingSplitPanel: overlap.panel.isDragging,
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
positionX,
|
|
||||||
positionY,
|
|
||||||
size?.width,
|
|
||||||
size?.height,
|
|
||||||
isButtonComponent,
|
|
||||||
type,
|
|
||||||
component,
|
|
||||||
getAdjustedX,
|
|
||||||
getOverlappingSplitPanel,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 너비 결정 로직: style.width (퍼센트) > 조건부 100% > size.width (픽셀)
|
// 너비 결정 로직: style.width (퍼센트) > 조건부 100% > size.width (픽셀)
|
||||||
const getWidth = () => {
|
const getWidth = () => {
|
||||||
// 1순위: style.width가 있으면 우선 사용 (퍼센트 값)
|
// 1순위: style.width가 있으면 우선 사용 (퍼센트 값)
|
||||||
|
|
@ -545,22 +437,18 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
const componentStyle = {
|
const componentStyle = {
|
||||||
position: "absolute" as const,
|
position: "absolute" as const,
|
||||||
...style, // 먼저 적용하고
|
...style, // 먼저 적용하고
|
||||||
left: adjustedPositionX, // 🆕 분할 패널 위 버튼은 조정된 X 좌표 사용
|
left: positionX,
|
||||||
top: positionY,
|
top: positionY,
|
||||||
width: getWidth(), // 우선순위에 따른 너비
|
width: getWidth(), // 우선순위에 따른 너비
|
||||||
height: getHeight(), // 우선순위에 따른 높이
|
height: getHeight(), // 우선순위에 따른 높이
|
||||||
zIndex: position?.z || 1,
|
zIndex: position?.z || 1,
|
||||||
// right 속성 강제 제거
|
// right 속성 강제 제거
|
||||||
right: undefined,
|
right: undefined,
|
||||||
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동
|
|
||||||
transition:
|
|
||||||
isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 선택된 컴포넌트 스타일
|
// 선택된 컴포넌트 스타일
|
||||||
// Section Paper는 자체적으로 선택 상태 테두리를 처리하므로 outline 제거
|
// Section Paper는 자체적으로 선택 상태 테두리를 처리하므로 outline 제거
|
||||||
const selectionStyle =
|
const selectionStyle = isSelected && !isSectionPaper
|
||||||
isSelected && !isSectionPaper
|
|
||||||
? {
|
? {
|
||||||
outline: "2px solid rgb(59, 130, 246)",
|
outline: "2px solid rgb(59, 130, 246)",
|
||||||
outlineOffset: "2px",
|
outlineOffset: "2px",
|
||||||
|
|
@ -593,7 +481,10 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* 컴포넌트 타입별 렌더링 */}
|
{/* 컴포넌트 타입별 렌더링 */}
|
||||||
<div ref={isFlowWidget ? contentRef : undefined} className="h-full w-full">
|
<div
|
||||||
|
ref={isFlowWidget ? contentRef : undefined}
|
||||||
|
className="h-full w-full"
|
||||||
|
>
|
||||||
{/* 영역 타입 */}
|
{/* 영역 타입 */}
|
||||||
{type === "area" && renderArea(component, children)}
|
{type === "area" && renderArea(component, children)}
|
||||||
|
|
||||||
|
|
@ -658,16 +549,16 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-auto w-full">
|
<div className="h-auto w-full">
|
||||||
<FlowWidget component={flowComponent as any} onSelectedDataChange={onFlowSelectedDataChange} />
|
<FlowWidget
|
||||||
|
component={flowComponent as any}
|
||||||
|
onSelectedDataChange={onFlowSelectedDataChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* 탭 컴포넌트 타입 */}
|
{/* 탭 컴포넌트 타입 */}
|
||||||
{(type === "tabs" ||
|
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
|
||||||
(type === "component" &&
|
|
||||||
((component as any).componentType === "tabs-widget" ||
|
|
||||||
(component as any).componentId === "tabs-widget"))) &&
|
|
||||||
(() => {
|
(() => {
|
||||||
console.log("🎯 탭 컴포넌트 조건 충족:", {
|
console.log("🎯 탭 컴포넌트 조건 충족:", {
|
||||||
type,
|
type,
|
||||||
|
|
@ -699,7 +590,9 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
<Badge key={tab.id} variant="outline" className="text-xs">
|
<Badge key={tab.id} variant="outline" className="text-xs">
|
||||||
{tab.label || `탭 ${index + 1}`}
|
{tab.label || `탭 ${index + 1}`}
|
||||||
{tab.screenName && (
|
{tab.screenName && (
|
||||||
<span className="ml-1 text-[10px] text-gray-400">({tab.screenName})</span>
|
<span className="ml-1 text-[10px] text-gray-400">
|
||||||
|
({tab.screenName})
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
@ -739,8 +632,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 컴포넌트 타입 - 레지스트리 기반 렌더링 (Section Paper, Section Card 등) */}
|
{/* 컴포넌트 타입 - 레지스트리 기반 렌더링 (Section Paper, Section Card 등) */}
|
||||||
{type === "component" &&
|
{type === "component" && (() => {
|
||||||
(() => {
|
|
||||||
const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer");
|
const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer");
|
||||||
return (
|
return (
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React from "react";
|
||||||
import { ComponentData, WebType, WidgetComponent } from "@/types/screen";
|
import { ComponentData, WebType, WidgetComponent } from "@/types/screen";
|
||||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
Building,
|
Building,
|
||||||
File,
|
File,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSplitPanel } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
||||||
|
|
||||||
// 컴포넌트 렌더러들 자동 등록
|
// 컴포넌트 렌더러들 자동 등록
|
||||||
import "@/lib/registry/components";
|
import "@/lib/registry/components";
|
||||||
|
|
@ -263,145 +262,14 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
}
|
}
|
||||||
: component;
|
: component;
|
||||||
|
|
||||||
// 🆕 분할 패널 리사이즈 Context
|
|
||||||
const splitPanelContext = useSplitPanel();
|
|
||||||
|
|
||||||
// 버튼 컴포넌트인지 확인 (분할 패널 위치 조정 대상)
|
|
||||||
const componentType = (component as any).componentType || "";
|
|
||||||
const componentId = (component as any).componentId || "";
|
|
||||||
const widgetType = (component as any).widgetType || "";
|
|
||||||
|
|
||||||
const isButtonComponent =
|
|
||||||
(type === "widget" && widgetType === "button") ||
|
|
||||||
(type === "component" &&
|
|
||||||
(["button-primary", "button-secondary"].includes(componentType) ||
|
|
||||||
["button-primary", "button-secondary"].includes(componentId)));
|
|
||||||
|
|
||||||
// 🆕 버튼이 처음 렌더링될 때의 분할 패널 정보를 기억 (기준점)
|
|
||||||
const initialPanelRatioRef = React.useRef<number | null>(null);
|
|
||||||
const initialPanelIdRef = React.useRef<string | null>(null);
|
|
||||||
// 버튼이 좌측 패널에 속하는지 여부 (한번 설정되면 유지)
|
|
||||||
const isInLeftPanelRef = React.useRef<boolean | null>(null);
|
|
||||||
|
|
||||||
// 🆕 분할 패널 위 버튼 위치 자동 조정
|
|
||||||
const calculateButtonPosition = () => {
|
|
||||||
// 버튼이 아니거나 분할 패널 컴포넌트 자체인 경우 조정하지 않음
|
|
||||||
const isSplitPanelComponent =
|
|
||||||
type === "component" && ["split-panel-layout", "split-panel-layout2"].includes(componentType);
|
|
||||||
|
|
||||||
if (!isButtonComponent || isSplitPanelComponent) {
|
|
||||||
return { adjustedPositionX: position.x, isOnSplitPanel: false, isDraggingSplitPanel: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentWidth = size?.width || 100;
|
|
||||||
const componentHeight = size?.height || 40;
|
|
||||||
|
|
||||||
// 분할 패널 위에 있는지 확인 (원래 위치 기준)
|
|
||||||
const overlap = splitPanelContext.getOverlappingSplitPanel(position.x, position.y, componentWidth, componentHeight);
|
|
||||||
|
|
||||||
// 분할 패널 위에 없으면 기준점 초기화
|
|
||||||
if (!overlap) {
|
|
||||||
if (initialPanelIdRef.current !== null) {
|
|
||||||
initialPanelRatioRef.current = null;
|
|
||||||
initialPanelIdRef.current = null;
|
|
||||||
isInLeftPanelRef.current = null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
adjustedPositionX: position.x,
|
|
||||||
isOnSplitPanel: false,
|
|
||||||
isDraggingSplitPanel: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { panel } = overlap;
|
|
||||||
|
|
||||||
// 🆕 초기 기준 비율 및 좌측 패널 소속 여부 설정 (처음 한 번만)
|
|
||||||
if (initialPanelIdRef.current !== overlap.panelId) {
|
|
||||||
initialPanelRatioRef.current = panel.leftWidthPercent;
|
|
||||||
initialPanelIdRef.current = overlap.panelId;
|
|
||||||
|
|
||||||
// 초기 배치 시 좌측 패널에 있는지 확인 (초기 비율 기준으로 계산)
|
|
||||||
// 현재 비율이 아닌, 버튼 원래 위치가 초기 좌측 패널 영역 안에 있었는지 판단
|
|
||||||
const initialLeftPanelWidth = (panel.width * panel.leftWidthPercent) / 100;
|
|
||||||
const componentCenterX = position.x + componentWidth / 2;
|
|
||||||
const relativeX = componentCenterX - panel.x;
|
|
||||||
const wasInLeftPanel = relativeX < initialLeftPanelWidth;
|
|
||||||
|
|
||||||
isInLeftPanelRef.current = wasInLeftPanel;
|
|
||||||
console.log("📌 [버튼 기준점 설정]:", {
|
|
||||||
componentId: component.id,
|
|
||||||
panelId: overlap.panelId,
|
|
||||||
initialRatio: panel.leftWidthPercent,
|
|
||||||
isInLeftPanel: wasInLeftPanel,
|
|
||||||
buttonCenterX: componentCenterX,
|
|
||||||
leftPanelWidth: initialLeftPanelWidth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 좌측 패널 소속이 아니면 조정하지 않음 (초기 배치 기준)
|
|
||||||
if (!isInLeftPanelRef.current) {
|
|
||||||
return {
|
|
||||||
adjustedPositionX: position.x,
|
|
||||||
isOnSplitPanel: true,
|
|
||||||
isDraggingSplitPanel: panel.isDragging,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 초기 기준 비율 (버튼이 처음 배치될 때의 비율)
|
|
||||||
const baseRatio = initialPanelRatioRef.current ?? panel.leftWidthPercent;
|
|
||||||
|
|
||||||
// 기준 비율 대비 현재 비율로 분할선 위치 계산
|
|
||||||
const baseDividerX = panel.x + (panel.width * baseRatio) / 100; // 초기 분할선 위치
|
|
||||||
const currentDividerX = panel.x + (panel.width * panel.leftWidthPercent) / 100; // 현재 분할선 위치
|
|
||||||
|
|
||||||
// 분할선 이동량 (px)
|
|
||||||
const dividerDelta = currentDividerX - baseDividerX;
|
|
||||||
|
|
||||||
// 변화가 없으면 원래 위치 반환
|
|
||||||
if (Math.abs(dividerDelta) < 1) {
|
|
||||||
return {
|
|
||||||
adjustedPositionX: position.x,
|
|
||||||
isOnSplitPanel: true,
|
|
||||||
isDraggingSplitPanel: panel.isDragging,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 버튼도 분할선과 같은 양만큼 이동
|
|
||||||
// 분할선이 왼쪽으로 100px 이동하면, 버튼도 왼쪽으로 100px 이동
|
|
||||||
const adjustedX = position.x + dividerDelta;
|
|
||||||
|
|
||||||
console.log("📍 [버튼 위치 조정]:", {
|
|
||||||
componentId: component.id,
|
|
||||||
originalX: position.x,
|
|
||||||
adjustedX,
|
|
||||||
dividerDelta,
|
|
||||||
baseRatio,
|
|
||||||
currentRatio: panel.leftWidthPercent,
|
|
||||||
baseDividerX,
|
|
||||||
currentDividerX,
|
|
||||||
isDragging: panel.isDragging,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
adjustedPositionX: adjustedX,
|
|
||||||
isOnSplitPanel: true,
|
|
||||||
isDraggingSplitPanel: panel.isDragging,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const { adjustedPositionX, isOnSplitPanel, isDraggingSplitPanel } = calculateButtonPosition();
|
|
||||||
|
|
||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
left: `${adjustedPositionX}px`, // 🆕 조정된 X 좌표 사용
|
left: `${position.x}px`,
|
||||||
top: `${position.y}px`,
|
top: `${position.y}px`,
|
||||||
...componentStyle, // componentStyle 전체 적용 (DynamicComponentRenderer에서 이미 size가 변환됨)
|
...componentStyle, // componentStyle 전체 적용 (DynamicComponentRenderer에서 이미 size가 변환됨)
|
||||||
width: getWidth(), // getWidth() 우선 (table-list 등 특수 케이스)
|
width: getWidth(), // getWidth() 우선 (table-list 등 특수 케이스)
|
||||||
height: getHeight(), // getHeight() 우선 (flow-widget 등 특수 케이스)
|
height: getHeight(), // getHeight() 우선 (flow-widget 등 특수 케이스)
|
||||||
zIndex: component.type === "layout" ? 1 : position.z || 2,
|
zIndex: component.type === "layout" ? 1 : position.z || 2,
|
||||||
right: undefined,
|
right: undefined,
|
||||||
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동
|
|
||||||
transition:
|
|
||||||
isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import { useSplitPanel } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
||||||
|
|
||||||
interface SplitPanelAwareWrapperProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
componentX: number;
|
|
||||||
componentY: number;
|
|
||||||
componentWidth: number;
|
|
||||||
componentHeight: number;
|
|
||||||
componentType?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 분할 패널 드래그 리사이즈에 따라 컴포넌트 위치를 자동 조정하는 래퍼
|
|
||||||
*
|
|
||||||
* 동작 방식:
|
|
||||||
* 1. 컴포넌트가 분할 패널의 좌측 영역 위에 있는지 감지
|
|
||||||
* 2. 좌측 영역 위에 있으면, 드래그 핸들 이동량만큼 X 좌표를 조정
|
|
||||||
* 3. 우측 영역이나 분할 패널 외부에 있으면 원래 위치 유지
|
|
||||||
*/
|
|
||||||
export const SplitPanelAwareWrapper: React.FC<SplitPanelAwareWrapperProps> = ({
|
|
||||||
children,
|
|
||||||
componentX,
|
|
||||||
componentY,
|
|
||||||
componentWidth,
|
|
||||||
componentHeight,
|
|
||||||
componentType,
|
|
||||||
style,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const { getAdjustedX, getOverlappingSplitPanel } = useSplitPanel();
|
|
||||||
|
|
||||||
// 분할 패널 위에 있는지 확인 및 조정된 X 좌표 계산
|
|
||||||
const { adjustedX, isInLeftPanel, isDragging } = useMemo(() => {
|
|
||||||
const overlap = getOverlappingSplitPanel(componentX, componentY, componentWidth, componentHeight);
|
|
||||||
|
|
||||||
if (!overlap) {
|
|
||||||
// 분할 패널 위에 없음
|
|
||||||
return { adjustedX: componentX, isInLeftPanel: false, isDragging: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overlap.isInLeftPanel) {
|
|
||||||
// 우측 패널 위에 있음 - 원래 위치 유지
|
|
||||||
return { adjustedX: componentX, isInLeftPanel: false, isDragging: overlap.panel.isDragging };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 좌측 패널 위에 있음 - 위치 조정
|
|
||||||
const adjusted = getAdjustedX(componentX, componentY, componentWidth, componentHeight);
|
|
||||||
|
|
||||||
return {
|
|
||||||
adjustedX: adjusted,
|
|
||||||
isInLeftPanel: true,
|
|
||||||
isDragging: overlap.panel.isDragging,
|
|
||||||
};
|
|
||||||
}, [componentX, componentY, componentWidth, componentHeight, getAdjustedX, getOverlappingSplitPanel]);
|
|
||||||
|
|
||||||
// 조정된 스타일
|
|
||||||
const adjustedStyle: React.CSSProperties = {
|
|
||||||
...style,
|
|
||||||
position: "absolute",
|
|
||||||
left: `${adjustedX}px`,
|
|
||||||
top: `${componentY}px`,
|
|
||||||
width: componentWidth,
|
|
||||||
height: componentHeight,
|
|
||||||
// 드래그 중에는 트랜지션 없이 즉시 이동, 드래그 끝나면 부드럽게
|
|
||||||
transition: isDragging ? "none" : "left 0.1s ease-out",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 디버그 로깅 (개발 중에만)
|
|
||||||
// if (isInLeftPanel) {
|
|
||||||
// console.log(`📍 [SplitPanelAwareWrapper] 위치 조정:`, {
|
|
||||||
// componentType,
|
|
||||||
// originalX: componentX,
|
|
||||||
// adjustedX,
|
|
||||||
// delta: adjustedX - componentX,
|
|
||||||
// isInLeftPanel,
|
|
||||||
// isDragging,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={adjustedStyle} className={className}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SplitPanelAwareWrapper;
|
|
||||||
|
|
@ -1,400 +0,0 @@
|
||||||
"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<string, SplitPanelInfo>;
|
|
||||||
|
|
||||||
// 분할 패널 등록/해제/업데이트
|
|
||||||
registerSplitPanel: (id: string, info: Omit<SplitPanelInfo, "id">) => void;
|
|
||||||
unregisterSplitPanel: (id: string) => void;
|
|
||||||
updateSplitPanel: (id: string, updates: Partial<SplitPanelInfo>) => 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<SplitPanelResizeContextValue | null>(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SplitPanelResize Context Provider
|
|
||||||
* 스크린 빌더 레벨에서 감싸서 사용
|
|
||||||
*/
|
|
||||||
export const SplitPanelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
||||||
// 등록된 분할 패널들
|
|
||||||
const splitPanelsRef = useRef<Map<string, SplitPanelInfo>>(new Map());
|
|
||||||
const [, forceUpdate] = useState(0);
|
|
||||||
|
|
||||||
// 레거시 호환용 상태
|
|
||||||
const [legacyLeftWidthPercent, setLegacyLeftWidthPercent] = useState(30);
|
|
||||||
const [legacyContainerRect, setLegacyContainerRect] = useState<DOMRect | null>(null);
|
|
||||||
const [legacyIsDragging, setLegacyIsDragging] = useState(false);
|
|
||||||
const [legacySplitPanelId, setLegacySplitPanelId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 분할 패널 등록
|
|
||||||
const registerSplitPanel = useCallback((id: string, info: Omit<SplitPanelInfo, "id">) => {
|
|
||||||
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<SplitPanelInfo>) => {
|
|
||||||
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<SplitPanelInfo> = { isDragging: dragging };
|
|
||||||
if (dragging) {
|
|
||||||
updates.initialLeftWidthPercent = panel.leftWidthPercent;
|
|
||||||
}
|
|
||||||
splitPanelsRef.current.set(firstPanelId, { ...panel, ...updates });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
forceUpdate((n) => n + 1);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = useMemo<SplitPanelResizeContextValue>(
|
|
||||||
() => ({
|
|
||||||
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 <SplitPanelResizeContext.Provider value={value}>{children}</SplitPanelResizeContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
||||||
import { ComponentRendererProps } from "../../types";
|
import { ComponentRendererProps } from "../../types";
|
||||||
import { SplitPanelLayoutConfig } from "./types";
|
import { SplitPanelLayoutConfig } from "./types";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -36,7 +36,6 @@ import { Label } from "@/components/ui/label";
|
||||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||||
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
|
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useSplitPanel } from "./SplitPanelContext";
|
|
||||||
|
|
||||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||||
// 추가 props
|
// 추가 props
|
||||||
|
|
@ -183,120 +182,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [leftWidth, setLeftWidth] = useState(splitRatio);
|
const [leftWidth, setLeftWidth] = useState(splitRatio);
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 🆕 SplitPanel Resize Context 연동 (버튼 등 외부 컴포넌트와 드래그 리사이즈 상태 공유)
|
|
||||||
const splitPanelContext = useSplitPanel();
|
|
||||||
const {
|
|
||||||
registerSplitPanel: ctxRegisterSplitPanel,
|
|
||||||
unregisterSplitPanel: ctxUnregisterSplitPanel,
|
|
||||||
updateSplitPanel: ctxUpdateSplitPanel,
|
|
||||||
} = splitPanelContext;
|
|
||||||
const splitPanelId = `split-panel-${component.id}`;
|
|
||||||
|
|
||||||
// 디버깅: Context 연결 상태 확인
|
|
||||||
console.log("🔗 [SplitPanelLayout] Context 연결 상태:", {
|
|
||||||
componentId: component.id,
|
|
||||||
splitPanelId,
|
|
||||||
hasRegisterFunc: typeof ctxRegisterSplitPanel === "function",
|
|
||||||
splitPanelsSize: splitPanelContext.splitPanels?.size ?? "없음",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Context에 분할 패널 등록 (좌표 정보 포함) - 마운트 시 1회만 실행
|
|
||||||
const ctxRegisterRef = useRef(ctxRegisterSplitPanel);
|
|
||||||
const ctxUnregisterRef = useRef(ctxUnregisterSplitPanel);
|
|
||||||
ctxRegisterRef.current = ctxRegisterSplitPanel;
|
|
||||||
ctxUnregisterRef.current = ctxUnregisterSplitPanel;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 컴포넌트의 위치와 크기 정보
|
|
||||||
const panelX = component.position?.x || 0;
|
|
||||||
const panelY = component.position?.y || 0;
|
|
||||||
const panelWidth = component.size?.width || component.style?.width || 800;
|
|
||||||
const panelHeight = component.size?.height || component.style?.height || 600;
|
|
||||||
|
|
||||||
const panelInfo = {
|
|
||||||
x: panelX,
|
|
||||||
y: panelY,
|
|
||||||
width: typeof panelWidth === "number" ? panelWidth : parseInt(String(panelWidth)) || 800,
|
|
||||||
height: typeof panelHeight === "number" ? panelHeight : parseInt(String(panelHeight)) || 600,
|
|
||||||
leftWidthPercent: splitRatio, // 초기값은 splitRatio 사용
|
|
||||||
initialLeftWidthPercent: splitRatio,
|
|
||||||
isDragging: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("📦 [SplitPanelLayout] Context에 분할 패널 등록:", {
|
|
||||||
splitPanelId,
|
|
||||||
panelInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctxRegisterRef.current(splitPanelId, panelInfo);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log("📦 [SplitPanelLayout] Context에서 분할 패널 해제:", splitPanelId);
|
|
||||||
ctxUnregisterRef.current(splitPanelId);
|
|
||||||
};
|
|
||||||
// 마운트/언마운트 시에만 실행, 위치/크기 변경은 별도 업데이트로 처리
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [splitPanelId]);
|
|
||||||
|
|
||||||
// 위치/크기 변경 시 Context 업데이트 (등록 후)
|
|
||||||
const ctxUpdateRef = useRef(ctxUpdateSplitPanel);
|
|
||||||
ctxUpdateRef.current = ctxUpdateSplitPanel;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const panelX = component.position?.x || 0;
|
|
||||||
const panelY = component.position?.y || 0;
|
|
||||||
const panelWidth = component.size?.width || component.style?.width || 800;
|
|
||||||
const panelHeight = component.size?.height || component.style?.height || 600;
|
|
||||||
|
|
||||||
ctxUpdateRef.current(splitPanelId, {
|
|
||||||
x: panelX,
|
|
||||||
y: panelY,
|
|
||||||
width: typeof panelWidth === "number" ? panelWidth : parseInt(String(panelWidth)) || 800,
|
|
||||||
height: typeof panelHeight === "number" ? panelHeight : parseInt(String(panelHeight)) || 600,
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
splitPanelId,
|
|
||||||
component.position?.x,
|
|
||||||
component.position?.y,
|
|
||||||
component.size?.width,
|
|
||||||
component.size?.height,
|
|
||||||
component.style?.width,
|
|
||||||
component.style?.height,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// leftWidth 변경 시 Context 업데이트
|
|
||||||
useEffect(() => {
|
|
||||||
ctxUpdateRef.current(splitPanelId, { leftWidthPercent: leftWidth });
|
|
||||||
}, [leftWidth, splitPanelId]);
|
|
||||||
|
|
||||||
// 드래그 상태 변경 시 Context 업데이트
|
|
||||||
// 이전 드래그 상태를 추적하여 드래그 종료 시점을 감지
|
|
||||||
const prevIsDraggingRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const wasJustDragging = prevIsDraggingRef.current && !isDragging;
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
// 드래그 시작 시: 현재 비율을 초기 비율로 저장
|
|
||||||
ctxUpdateRef.current(splitPanelId, {
|
|
||||||
isDragging: true,
|
|
||||||
initialLeftWidthPercent: leftWidth,
|
|
||||||
});
|
|
||||||
} else if (wasJustDragging) {
|
|
||||||
// 드래그 종료 시: 최종 비율을 초기 비율로 업데이트 (버튼 위치 고정)
|
|
||||||
ctxUpdateRef.current(splitPanelId, {
|
|
||||||
isDragging: false,
|
|
||||||
initialLeftWidthPercent: leftWidth,
|
|
||||||
});
|
|
||||||
console.log("🛑 [SplitPanelLayout] 드래그 종료 - 버튼 위치 고정:", {
|
|
||||||
splitPanelId,
|
|
||||||
finalLeftWidthPercent: leftWidth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
prevIsDraggingRef.current = isDragging;
|
|
||||||
}, [isDragging, splitPanelId, leftWidth]);
|
|
||||||
|
|
||||||
// 🆕 그룹별 합산된 데이터 계산
|
// 🆕 그룹별 합산된 데이터 계산
|
||||||
const summedLeftData = useMemo(() => {
|
const summedLeftData = useMemo(() => {
|
||||||
console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig);
|
console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig);
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,3 @@ export type { SplitPanelLayoutConfig } from "./types";
|
||||||
// 컴포넌트 내보내기
|
// 컴포넌트 내보내기
|
||||||
export { SplitPanelLayoutComponent } from "./SplitPanelLayoutComponent";
|
export { SplitPanelLayoutComponent } from "./SplitPanelLayoutComponent";
|
||||||
export { SplitPanelLayoutRenderer } from "./SplitPanelLayoutRenderer";
|
export { SplitPanelLayoutRenderer } from "./SplitPanelLayoutRenderer";
|
||||||
|
|
||||||
// Resize Context 내보내기 (버튼 등 외부 컴포넌트에서 분할 패널 드래그 리사이즈 상태 활용)
|
|
||||||
export {
|
|
||||||
SplitPanelProvider,
|
|
||||||
useSplitPanel,
|
|
||||||
useAdjustedPosition,
|
|
||||||
useSplitPanelAwarePosition,
|
|
||||||
useAdjustedComponentPosition,
|
|
||||||
} from "./SplitPanelContext";
|
|
||||||
export type { SplitPanelResizeContextValue, SplitPanelInfo } from "./SplitPanelContext";
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue