조건부 컨테이너

This commit is contained in:
kjs 2025-11-17 10:09:02 +09:00
parent d1ce14de7a
commit 2c099feea0
4 changed files with 97 additions and 39 deletions

View File

@ -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}>

View File

@ -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>
); );
} }

View File

@ -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",
}, },
}, },

View File

@ -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;