분할패널 버튼 이동 가능하게 수정
This commit is contained in:
@@ -50,6 +50,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
|
||||
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
||||
|
||||
/**
|
||||
* 🔗 연쇄 드롭다운 래퍼 컴포넌트
|
||||
@@ -2101,113 +2102,115 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
: component;
|
||||
|
||||
return (
|
||||
<TableOptionsProvider>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 테이블 옵션 툴바 */}
|
||||
<TableOptionsToolbar />
|
||||
|
||||
{/* 메인 컨텐츠 */}
|
||||
<div className="h-full flex-1" style={{ width: '100%' }}>
|
||||
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
|
||||
{shouldShowLabel && (
|
||||
<label className="mb-2 block text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{labelText}
|
||||
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */}
|
||||
<div className="h-full" style={{ width: '100%', height: '100%' }}>{renderInteractiveWidget(componentForRendering)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 개선된 검증 패널 (선택적 표시) */}
|
||||
{showValidationPanel && enhancedValidation && (
|
||||
<div className="absolute bottom-4 right-4 z-50">
|
||||
<FormValidationIndicator
|
||||
validationState={enhancedValidation.validationState}
|
||||
saveState={enhancedValidation.saveState}
|
||||
onSave={async () => {
|
||||
const success = await enhancedValidation.saveForm();
|
||||
if (success) {
|
||||
toast.success("데이터가 성공적으로 저장되었습니다!");
|
||||
}
|
||||
}}
|
||||
canSave={enhancedValidation.canSave}
|
||||
compact={true}
|
||||
showDetails={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모달 화면 */}
|
||||
<Dialog open={!!popupScreen} onOpenChange={() => {
|
||||
setPopupScreen(null);
|
||||
setPopupFormData({}); // 팝업 닫을 때 formData도 초기화
|
||||
}}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-4 pb-2">
|
||||
<DialogTitle>{popupScreen?.title || "상세 정보"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SplitPanelProvider>
|
||||
<TableOptionsProvider>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 테이블 옵션 툴바 */}
|
||||
<TableOptionsToolbar />
|
||||
|
||||
<div className="overflow-y-auto px-6 pb-6" style={{ maxHeight: "calc(90vh - 80px)" }}>
|
||||
{popupLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">화면을 불러오는 중...</div>
|
||||
</div>
|
||||
) : popupLayout.length > 0 ? (
|
||||
<div className="relative bg-background border rounded" style={{
|
||||
width: popupScreenResolution ? `${popupScreenResolution.width}px` : "100%",
|
||||
height: popupScreenResolution ? `${popupScreenResolution.height}px` : "400px",
|
||||
minHeight: "400px",
|
||||
position: "relative",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{/* 팝업에서도 실제 위치와 크기로 렌더링 */}
|
||||
{popupLayout.map((popupComponent) => (
|
||||
<div
|
||||
key={popupComponent.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${popupComponent.position.x}px`,
|
||||
top: `${popupComponent.position.y}px`,
|
||||
width: popupComponent.style?.width || `${popupComponent.size.width}px`,
|
||||
height: popupComponent.style?.height || `${popupComponent.size.height}px`,
|
||||
zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한
|
||||
}}
|
||||
>
|
||||
{/* 🎯 핵심 수정: 팝업 전용 formData 사용 */}
|
||||
<InteractiveScreenViewer
|
||||
component={popupComponent}
|
||||
allComponents={popupLayout}
|
||||
hideLabel={false}
|
||||
screenInfo={popupScreenInfo || undefined}
|
||||
formData={popupFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("💾 팝업 formData 업데이트:", {
|
||||
fieldName,
|
||||
value,
|
||||
valueType: typeof value,
|
||||
prevFormData: popupFormData
|
||||
});
|
||||
|
||||
setPopupFormData(prev => ({
|
||||
...prev,
|
||||
[fieldName]: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">화면 데이터가 없습니다.</div>
|
||||
</div>
|
||||
{/* 메인 컨텐츠 */}
|
||||
<div className="h-full flex-1" style={{ width: '100%' }}>
|
||||
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
|
||||
{shouldShowLabel && (
|
||||
<label className="mb-2 block text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{labelText}
|
||||
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */}
|
||||
<div className="h-full" style={{ width: '100%', height: '100%' }}>{renderInteractiveWidget(componentForRendering)}</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableOptionsProvider>
|
||||
</div>
|
||||
|
||||
{/* 개선된 검증 패널 (선택적 표시) */}
|
||||
{showValidationPanel && enhancedValidation && (
|
||||
<div className="absolute bottom-4 right-4 z-50">
|
||||
<FormValidationIndicator
|
||||
validationState={enhancedValidation.validationState}
|
||||
saveState={enhancedValidation.saveState}
|
||||
onSave={async () => {
|
||||
const success = await enhancedValidation.saveForm();
|
||||
if (success) {
|
||||
toast.success("데이터가 성공적으로 저장되었습니다!");
|
||||
}
|
||||
}}
|
||||
canSave={enhancedValidation.canSave}
|
||||
compact={true}
|
||||
showDetails={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모달 화면 */}
|
||||
<Dialog open={!!popupScreen} onOpenChange={() => {
|
||||
setPopupScreen(null);
|
||||
setPopupFormData({}); // 팝업 닫을 때 formData도 초기화
|
||||
}}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden p-0">
|
||||
<DialogHeader className="px-6 pt-4 pb-2">
|
||||
<DialogTitle>{popupScreen?.title || "상세 정보"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto px-6 pb-6" style={{ maxHeight: "calc(90vh - 80px)" }}>
|
||||
{popupLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">화면을 불러오는 중...</div>
|
||||
</div>
|
||||
) : popupLayout.length > 0 ? (
|
||||
<div className="relative bg-background border rounded" style={{
|
||||
width: popupScreenResolution ? `${popupScreenResolution.width}px` : "100%",
|
||||
height: popupScreenResolution ? `${popupScreenResolution.height}px` : "400px",
|
||||
minHeight: "400px",
|
||||
position: "relative",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{/* 팝업에서도 실제 위치와 크기로 렌더링 */}
|
||||
{popupLayout.map((popupComponent) => (
|
||||
<div
|
||||
key={popupComponent.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${popupComponent.position.x}px`,
|
||||
top: `${popupComponent.position.y}px`,
|
||||
width: popupComponent.style?.width || `${popupComponent.size.width}px`,
|
||||
height: popupComponent.style?.height || `${popupComponent.size.height}px`,
|
||||
zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한
|
||||
}}
|
||||
>
|
||||
{/* 🎯 핵심 수정: 팝업 전용 formData 사용 */}
|
||||
<InteractiveScreenViewer
|
||||
component={popupComponent}
|
||||
allComponents={popupLayout}
|
||||
hideLabel={false}
|
||||
screenInfo={popupScreenInfo || undefined}
|
||||
formData={popupFormData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("💾 팝업 formData 업데이트:", {
|
||||
fieldName,
|
||||
value,
|
||||
valueType: typeof value,
|
||||
prevFormData: popupFormData
|
||||
});
|
||||
|
||||
setPopupFormData(prev => ({
|
||||
...prev,
|
||||
[fieldName]: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">화면 데이터가 없습니다.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableOptionsProvider>
|
||||
</SplitPanelProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { ComponentData, WebType, isWidgetComponent, isContainerComponent } from "@/types";
|
||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,6 +14,7 @@ import { FileUpload } from "./widgets/FileUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
|
||||
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
|
||||
import { useSplitPanel } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
||||
import {
|
||||
Database,
|
||||
Type,
|
||||
@@ -110,8 +111,8 @@ const renderArea = (component: ComponentData, children?: React.ReactNode) => {
|
||||
};
|
||||
|
||||
// 동적 웹 타입 위젯 렌더링 컴포넌트
|
||||
const WidgetRenderer: React.FC<{
|
||||
component: ComponentData;
|
||||
const WidgetRenderer: React.FC<{
|
||||
component: ComponentData;
|
||||
isDesignMode?: boolean;
|
||||
sortBy?: string;
|
||||
sortOrder?: "asc" | "desc";
|
||||
@@ -253,22 +254,23 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
|
||||
// 플로우 위젯의 실제 높이 측정
|
||||
useEffect(() => {
|
||||
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||
|
||||
const isFlowWidget =
|
||||
type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||
|
||||
if (isFlowWidget && contentRef.current) {
|
||||
const measureHeight = () => {
|
||||
if (contentRef.current) {
|
||||
// getBoundingClientRect()로 실제 렌더링된 높이 측정
|
||||
const rect = contentRef.current.getBoundingClientRect();
|
||||
const measured = rect.height;
|
||||
|
||||
|
||||
// scrollHeight도 함께 확인하여 더 큰 값 사용
|
||||
const scrollHeight = contentRef.current.scrollHeight;
|
||||
const rawHeight = Math.max(measured, scrollHeight);
|
||||
|
||||
|
||||
// 40px 단위로 올림
|
||||
const finalHeight = Math.ceil(rawHeight / 40) * 40;
|
||||
|
||||
|
||||
if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) {
|
||||
setActualHeight(finalHeight);
|
||||
}
|
||||
@@ -400,12 +402,118 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
}, [component.id, fileUpdateTrigger]);
|
||||
|
||||
// 컴포넌트 스타일 계산
|
||||
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||
const isFlowWidget =
|
||||
type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||
const isSectionPaper = type === "component" && (component as any).componentConfig?.type === "section-paper";
|
||||
|
||||
|
||||
const positionX = position?.x || 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 (픽셀)
|
||||
const getWidth = () => {
|
||||
// 1순위: style.width가 있으면 우선 사용 (퍼센트 값)
|
||||
@@ -437,23 +545,27 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
const componentStyle = {
|
||||
position: "absolute" as const,
|
||||
...style, // 먼저 적용하고
|
||||
left: positionX,
|
||||
left: adjustedPositionX, // 🆕 분할 패널 위 버튼은 조정된 X 좌표 사용
|
||||
top: positionY,
|
||||
width: getWidth(), // 우선순위에 따른 너비
|
||||
height: getHeight(), // 우선순위에 따른 높이
|
||||
zIndex: position?.z || 1,
|
||||
// right 속성 강제 제거
|
||||
right: undefined,
|
||||
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동
|
||||
transition:
|
||||
isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined,
|
||||
};
|
||||
|
||||
// 선택된 컴포넌트 스타일
|
||||
// Section Paper는 자체적으로 선택 상태 테두리를 처리하므로 outline 제거
|
||||
const selectionStyle = isSelected && !isSectionPaper
|
||||
? {
|
||||
outline: "2px solid rgb(59, 130, 246)",
|
||||
outlineOffset: "2px",
|
||||
}
|
||||
: {};
|
||||
const selectionStyle =
|
||||
isSelected && !isSectionPaper
|
||||
? {
|
||||
outline: "2px solid rgb(59, 130, 246)",
|
||||
outlineOffset: "2px",
|
||||
}
|
||||
: {};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// 컴포넌트 영역 내에서만 클릭 이벤트 처리
|
||||
@@ -481,10 +593,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
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)}
|
||||
|
||||
@@ -549,16 +658,16 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
|
||||
return (
|
||||
<div className="h-auto w-full">
|
||||
<FlowWidget
|
||||
component={flowComponent as any}
|
||||
onSelectedDataChange={onFlowSelectedDataChange}
|
||||
/>
|
||||
<FlowWidget component={flowComponent as any} onSelectedDataChange={onFlowSelectedDataChange} />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 탭 컴포넌트 타입 */}
|
||||
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
|
||||
{(type === "tabs" ||
|
||||
(type === "component" &&
|
||||
((component as any).componentType === "tabs-widget" ||
|
||||
(component as any).componentId === "tabs-widget"))) &&
|
||||
(() => {
|
||||
console.log("🎯 탭 컴포넌트 조건 충족:", {
|
||||
type,
|
||||
@@ -590,9 +699,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
<Badge key={tab.id} variant="outline" className="text-xs">
|
||||
{tab.label || `탭 ${index + 1}`}
|
||||
{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>
|
||||
))}
|
||||
@@ -632,28 +739,29 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
)}
|
||||
|
||||
{/* 컴포넌트 타입 - 레지스트리 기반 렌더링 (Section Paper, Section Card 등) */}
|
||||
{type === "component" && (() => {
|
||||
const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer");
|
||||
return (
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isSelected={isSelected}
|
||||
isDesignMode={isDesignMode}
|
||||
onClick={onClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</DynamicComponentRenderer>
|
||||
);
|
||||
})()}
|
||||
{type === "component" &&
|
||||
(() => {
|
||||
const { DynamicComponentRenderer } = require("@/lib/registry/DynamicComponentRenderer");
|
||||
return (
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isSelected={isSelected}
|
||||
isDesignMode={isDesignMode}
|
||||
onClick={onClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</DynamicComponentRenderer>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */}
|
||||
{type === "widget" && !isFileComponent(component) && (
|
||||
<div className="h-full w-full">
|
||||
<WidgetRenderer
|
||||
component={component}
|
||||
<WidgetRenderer
|
||||
component={component}
|
||||
isDesignMode={isDesignMode}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { ComponentData, WebType, WidgetComponent } from "@/types/screen";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Building,
|
||||
File,
|
||||
} from "lucide-react";
|
||||
import { useSplitPanel } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
||||
|
||||
// 컴포넌트 렌더러들 자동 등록
|
||||
import "@/lib/registry/components";
|
||||
@@ -60,7 +61,7 @@ interface RealtimePreviewProps {
|
||||
sortBy?: string;
|
||||
sortOrder?: "asc" | "desc";
|
||||
columnOrder?: string[];
|
||||
|
||||
|
||||
// 🆕 조건부 컨테이너 높이 변화 콜백
|
||||
onHeightChange?: (componentId: string, newHeight: number) => void;
|
||||
}
|
||||
@@ -262,14 +263,145 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
}
|
||||
: 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 = {
|
||||
left: `${position.x}px`,
|
||||
left: `${adjustedPositionX}px`, // 🆕 조정된 X 좌표 사용
|
||||
top: `${position.y}px`,
|
||||
...componentStyle, // componentStyle 전체 적용 (DynamicComponentRenderer에서 이미 size가 변환됨)
|
||||
width: getWidth(), // getWidth() 우선 (table-list 등 특수 케이스)
|
||||
height: getHeight(), // getHeight() 우선 (flow-widget 등 특수 케이스)
|
||||
zIndex: component.type === "layout" ? 1 : position.z || 2,
|
||||
right: undefined,
|
||||
// 🆕 분할 패널 드래그 중에는 트랜지션 없이 즉시 이동
|
||||
transition:
|
||||
isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined,
|
||||
};
|
||||
|
||||
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
||||
|
||||
92
frontend/components/screen/SplitPanelAwareWrapper.tsx
Normal file
92
frontend/components/screen/SplitPanelAwareWrapper.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"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;
|
||||
Reference in New Issue
Block a user