조건부 컨테이너

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";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
@ -34,6 +34,8 @@ export function ConditionalContainerComponent({
onDeleteComponent,
onSelectComponent,
selectedComponentId,
onHeightChange,
componentId,
style,
className,
}: 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 = {
@ -80,6 +109,7 @@ export function ConditionalContainerComponent({
return (
<div
ref={containerRef}
className={cn("h-full w-full flex flex-col", spacingClass, className)}
style={style}
>
@ -106,7 +136,7 @@ export function ConditionalContainerComponent({
</div>
{/* 조건별 섹션들 */}
<div className="flex-1 min-h-0 overflow-auto">
<div className="flex-1 min-h-0">
{isDesignMode ? (
// 디자인 모드: 모든 섹션 표시
<div className={spacingClass}>

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from "react";
import { ConditionalSectionViewerProps } from "./types";
import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewer";
import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
@ -27,32 +27,33 @@ export function ConditionalSectionViewer({
const [isLoading, setIsLoading] = useState(false);
const [components, setComponents] = useState<ComponentData[]>([]);
const [screenInfo, setScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
const [screenResolution, setScreenResolution] = useState<{ width: number; height: number } | null>(null);
// 화면 로드
useEffect(() => {
if (!screenId) {
setComponents([]);
setScreenInfo(null);
setScreenResolution(null);
return;
}
const loadScreen = async () => {
setIsLoading(true);
try {
const [layout, screen] = await Promise.all([
screenApi.getLayout(screenId),
screenApi.getScreen(screenId),
]);
const [layout, screen] = await Promise.all([screenApi.getLayout(screenId), screenApi.getScreen(screenId)]);
setComponents(layout.components || []);
setScreenInfo({
id: screenId,
tableName: screen.tableName,
});
setScreenResolution(layout.screenResolution || null);
} catch (error) {
console.error("화면 로드 실패:", error);
setComponents([]);
setScreenInfo(null);
setScreenResolution(null);
} finally {
setIsLoading(false);
}
@ -69,20 +70,18 @@ export function ConditionalSectionViewer({
return (
<div
className={cn(
"relative min-h-[200px] transition-all",
showBorder && "rounded-lg border-2",
isDesignMode ? (
"border-dashed border-muted-foreground/30 bg-muted/20"
) : (
showBorder ? "border-border bg-card" : ""
),
!isDesignMode && !isActive && "hidden"
"relative w-full transition-all",
isDesignMode && showBorder && "border-muted-foreground/30 bg-muted/20 rounded-lg border-2 border-dashed",
!isDesignMode && !isActive && "hidden",
)}
style={{
minHeight: isDesignMode ? "200px" : undefined,
}}
data-section-id={sectionId}
>
{/* 섹션 라벨 (디자인 모드에서만 표시) */}
{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 && "(활성)"}
{screenId && ` - 화면 ID: ${screenId}`}
</div>
@ -91,40 +90,65 @@ export function ConditionalSectionViewer({
{/* 화면 미선택 안내 (디자인 모드 + 화면 없을 때) */}
{isDesignMode && !screenId && (
<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-xs mt-1">: {condition}</p>
<p className="mt-1 text-xs">: {condition}</p>
</div>
</div>
)}
{/* 로딩 중 */}
{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">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<p className="text-xs text-muted-foreground"> ...</p>
<Loader2 className="text-primary h-6 w-6 animate-spin" />
<p className="text-muted-foreground text-xs"> ...</p>
</div>
</div>
)}
{/* 화면 렌더링 */}
{screenId && components.length > 0 && (
<div className="relative min-h-[200px] w-full">
{components.map((component) => (
<InteractiveScreenViewer
key={component.id}
component={component}
allComponents={components}
formData={formData}
onFormDataChange={onFormDataChange}
hideLabel={false}
screenInfo={screenInfo || undefined}
/>
))}
</div>
<>
{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) => (
<RealtimePreview
key={component.id}
component={component}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
/>
))}
</div>
</div>
)}
</>
)}
</div>
);
}

View File

@ -20,8 +20,8 @@ export const ConditionalContainerDefinition: Omit<
tags: ["조건부", "분기", "동적", "레이아웃"],
defaultSize: {
width: 800,
height: 600,
width: 1400,
height: 800,
},
defaultConfig: {
@ -48,8 +48,8 @@ export const ConditionalContainerDefinition: Omit<
defaultProps: {
style: {
width: "800px",
height: "600px",
width: "1400px",
height: "800px",
},
},

View File

@ -53,6 +53,10 @@ export interface ConditionalContainerProps {
onSelectComponent?: (componentId: string) => void;
selectedComponentId?: string;
// 높이 변화 알림 (아래 컴포넌트 재배치용)
onHeightChange?: (newHeight: number) => void;
componentId?: string; // 자신의 컴포넌트 ID
// 스타일
style?: React.CSSProperties;
className?: string;