조건부 컨테이너
This commit is contained in:
parent
d1ce14de7a
commit
2c099feea0
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -34,6 +34,8 @@ export function ConditionalContainerComponent({
|
||||||
onDeleteComponent,
|
onDeleteComponent,
|
||||||
onSelectComponent,
|
onSelectComponent,
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
|
onHeightChange,
|
||||||
|
componentId,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
}: ConditionalContainerProps) {
|
}: ConditionalContainerProps) {
|
||||||
|
|
@ -70,6 +72,33 @@ export function ConditionalContainerComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 컨테이너 높이 측정용 ref
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const previousHeightRef = useRef<number>(0);
|
||||||
|
|
||||||
|
// 높이 변화 감지 및 콜백 호출
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || isDesignMode || !onHeightChange) return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const newHeight = entry.contentRect.height;
|
||||||
|
|
||||||
|
// 높이가 실제로 변경되었을 때만 콜백 호출
|
||||||
|
if (Math.abs(newHeight - previousHeightRef.current) > 5) {
|
||||||
|
console.log(`📏 조건부 컨테이너 높이 변화: ${previousHeightRef.current}px → ${newHeight}px`);
|
||||||
|
previousHeightRef.current = newHeight;
|
||||||
|
onHeightChange(newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [isDesignMode, onHeightChange, selectedValue]); // selectedValue 변경 시에도 감지
|
||||||
|
|
||||||
// 간격 스타일
|
// 간격 스타일
|
||||||
const spacingClass = {
|
const spacingClass = {
|
||||||
|
|
@ -80,6 +109,7 @@ export function ConditionalContainerComponent({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={cn("h-full w-full flex flex-col", spacingClass, className)}
|
className={cn("h-full w-full flex flex-col", spacingClass, className)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
|
|
@ -106,7 +136,7 @@ export function ConditionalContainerComponent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 조건별 섹션들 */}
|
{/* 조건별 섹션들 */}
|
||||||
<div className="flex-1 min-h-0 overflow-auto">
|
<div className="flex-1 min-h-0">
|
||||||
{isDesignMode ? (
|
{isDesignMode ? (
|
||||||
// 디자인 모드: 모든 섹션 표시
|
// 디자인 모드: 모든 섹션 표시
|
||||||
<div className={spacingClass}>
|
<div className={spacingClass}>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { ConditionalSectionViewerProps } from "./types";
|
import { ConditionalSectionViewerProps } from "./types";
|
||||||
import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewer";
|
import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
|
@ -27,32 +27,33 @@ export function ConditionalSectionViewer({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [components, setComponents] = useState<ComponentData[]>([]);
|
const [components, setComponents] = useState<ComponentData[]>([]);
|
||||||
const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
|
const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
|
||||||
|
const [screenResolution, setScreenResolution] = useState<{ width: number; height: number } | null>(null);
|
||||||
|
|
||||||
// 화면 로드
|
// 화면 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!screenId) {
|
if (!screenId) {
|
||||||
setComponents([]);
|
setComponents([]);
|
||||||
setScreenInfo(null);
|
setScreenInfo(null);
|
||||||
|
setScreenResolution(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadScreen = async () => {
|
const loadScreen = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [layout, screen] = await Promise.all([
|
const [layout, screen] = await Promise.all([screenApi.getLayout(screenId), screenApi.getScreen(screenId)]);
|
||||||
screenApi.getLayout(screenId),
|
|
||||||
screenApi.getScreen(screenId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setComponents(layout.components || []);
|
setComponents(layout.components || []);
|
||||||
setScreenInfo({
|
setScreenInfo({
|
||||||
id: screenId,
|
id: screenId,
|
||||||
tableName: screen.tableName,
|
tableName: screen.tableName,
|
||||||
});
|
});
|
||||||
|
setScreenResolution(layout.screenResolution || null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("화면 로드 실패:", error);
|
console.error("화면 로드 실패:", error);
|
||||||
setComponents([]);
|
setComponents([]);
|
||||||
setScreenInfo(null);
|
setScreenInfo(null);
|
||||||
|
setScreenResolution(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -69,20 +70,18 @@ export function ConditionalSectionViewer({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative min-h-[200px] transition-all",
|
"relative w-full transition-all",
|
||||||
showBorder && "rounded-lg border-2",
|
isDesignMode && showBorder && "border-muted-foreground/30 bg-muted/20 rounded-lg border-2 border-dashed",
|
||||||
isDesignMode ? (
|
!isDesignMode && !isActive && "hidden",
|
||||||
"border-dashed border-muted-foreground/30 bg-muted/20"
|
|
||||||
) : (
|
|
||||||
showBorder ? "border-border bg-card" : ""
|
|
||||||
),
|
|
||||||
!isDesignMode && !isActive && "hidden"
|
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
minHeight: isDesignMode ? "200px" : undefined,
|
||||||
|
}}
|
||||||
data-section-id={sectionId}
|
data-section-id={sectionId}
|
||||||
>
|
>
|
||||||
{/* 섹션 라벨 (디자인 모드에서만 표시) */}
|
{/* 섹션 라벨 (디자인 모드에서만 표시) */}
|
||||||
{isDesignMode && (
|
{isDesignMode && (
|
||||||
<div className="absolute -top-3 left-4 bg-background px-2 text-xs font-medium text-muted-foreground z-10">
|
<div className="bg-background text-muted-foreground absolute -top-3 left-4 z-10 px-2 text-xs font-medium">
|
||||||
{label} {isActive && "(활성)"}
|
{label} {isActive && "(활성)"}
|
||||||
{screenId && ` - 화면 ID: ${screenId}`}
|
{screenId && ` - 화면 ID: ${screenId}`}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,40 +90,65 @@ export function ConditionalSectionViewer({
|
||||||
{/* 화면 미선택 안내 (디자인 모드 + 화면 없을 때) */}
|
{/* 화면 미선택 안내 (디자인 모드 + 화면 없을 때) */}
|
||||||
{isDesignMode && !screenId && (
|
{isDesignMode && !screenId && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="text-center text-muted-foreground">
|
<div className="text-muted-foreground text-center">
|
||||||
<p className="text-sm">설정 패널에서 화면을 선택하세요</p>
|
<p className="text-sm">설정 패널에서 화면을 선택하세요</p>
|
||||||
<p className="text-xs mt-1">조건: {condition}</p>
|
<p className="mt-1 text-xs">조건: {condition}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 로딩 중 */}
|
{/* 로딩 중 */}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-20">
|
<div className="bg-background/50 absolute inset-0 z-20 flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||||
<p className="text-xs text-muted-foreground">화면 로드 중...</p>
|
<p className="text-muted-foreground text-xs">화면 로드 중...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 화면 렌더링 */}
|
{/* 화면 렌더링 */}
|
||||||
{screenId && components.length > 0 && (
|
{screenId && components.length > 0 && (
|
||||||
<div className="relative min-h-[200px] w-full">
|
<>
|
||||||
|
{isDesignMode ? (
|
||||||
|
/* 디자인 모드: 화면 정보만 표시 */
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-foreground mb-2 text-sm font-medium">{screenName || `화면 ID: ${screenId}`}</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{screenResolution?.width} x {screenResolution?.height}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">컴포넌트 {components.length}개</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 실행 모드: 실제 화면 렌더링 */
|
||||||
|
<div className="w-full">
|
||||||
|
{/* 화면 크기만큼의 절대 위치 캔버스 */}
|
||||||
|
<div
|
||||||
|
className="relative mx-auto"
|
||||||
|
style={{
|
||||||
|
width: screenResolution?.width ? `${screenResolution.width}px` : "100%",
|
||||||
|
height: screenResolution?.height ? `${screenResolution.height}px` : "auto",
|
||||||
|
minHeight: "200px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{components.map((component) => (
|
{components.map((component) => (
|
||||||
<InteractiveScreenViewer
|
<RealtimePreview
|
||||||
key={component.id}
|
key={component.id}
|
||||||
component={component}
|
component={component}
|
||||||
allComponents={components}
|
isSelected={false}
|
||||||
formData={formData}
|
isDesignMode={false}
|
||||||
onFormDataChange={onFormDataChange}
|
onClick={() => {}}
|
||||||
hideLabel={false}
|
screenId={screenInfo?.id}
|
||||||
screenInfo={screenInfo || undefined}
|
tableName={screenInfo?.tableName}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ export const ConditionalContainerDefinition: Omit<
|
||||||
tags: ["조건부", "분기", "동적", "레이아웃"],
|
tags: ["조건부", "분기", "동적", "레이아웃"],
|
||||||
|
|
||||||
defaultSize: {
|
defaultSize: {
|
||||||
width: 800,
|
width: 1400,
|
||||||
height: 600,
|
height: 800,
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
|
|
@ -48,8 +48,8 @@ export const ConditionalContainerDefinition: Omit<
|
||||||
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
style: {
|
style: {
|
||||||
width: "800px",
|
width: "1400px",
|
||||||
height: "600px",
|
height: "800px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ export interface ConditionalContainerProps {
|
||||||
onSelectComponent?: (componentId: string) => void;
|
onSelectComponent?: (componentId: string) => void;
|
||||||
selectedComponentId?: string;
|
selectedComponentId?: string;
|
||||||
|
|
||||||
|
// 높이 변화 알림 (아래 컴포넌트 재배치용)
|
||||||
|
onHeightChange?: (newHeight: number) => void;
|
||||||
|
componentId?: string; // 자신의 컴포넌트 ID
|
||||||
|
|
||||||
// 스타일
|
// 스타일
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue