Files
vexplor/frontend/components/screen/widgets/TabsWidget.tsx
kjs 6901baab8e feat(screen-designer): 그리드 컬럼 시스템 개선 및 컴포넌트 너비 렌더링 수정
주요 변경사항:
- 격자 설정을 편집 탭에서 항상 표시 (해상도 설정 하단)
- 그리드 컬럼 수 동적 조정 가능 (1-24)
- 컴포넌트 생성 시 현재 그리드 컬럼 수 기반 자동 계산
- 컴포넌트 너비가 설정한 컬럼 수대로 정확히 표시되도록 수정

수정된 파일:
- ScreenDesigner: 컴포넌트 드롭 시 gridColumns와 style.width 동적 계산
- UnifiedPropertiesPanel: 격자 설정 UI 통합, 차지 컬럼 수 설정 시 width 자동 계산
- RealtimePreviewDynamic: getWidth 우선순위 수정, DOM 크기 디버깅 로그 추가
- 8개 컴포넌트: componentStyle.width를 항상 100%로 고정
  * ButtonPrimaryComponent
  * TextInputComponent
  * NumberInputComponent
  * TextareaBasicComponent
  * DateInputComponent
  * TableListComponent
  * CardDisplayComponent

문제 해결:
- 컴포넌트 내부에서 component.style.width를 재사용하여 이중 축소 발생
- 해결: 부모 컨테이너(RealtimePreviewDynamic)가 width 제어, 컴포넌트는 항상 100%
- 결과: 파란 테두리와 내부 콘텐츠가 동일한 크기로 정확히 표시
2025-11-04 16:17:19 +09:00

211 lines
6.9 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { TabsComponent, TabItem, ScreenDefinition } from "@/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loader2, FileQuestion } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
interface TabsWidgetProps {
component: TabsComponent;
isPreview?: boolean;
}
/**
* 탭 위젯 컴포넌트
* 각 탭에 다른 화면을 표시할 수 있습니다
*/
export const TabsWidget: React.FC<TabsWidgetProps> = ({ component, isPreview = false }) => {
// componentConfig에서 설정 읽기 (새 컴포넌트 시스템)
const config = (component as any).componentConfig || component;
const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config;
// console.log("🔍 TabsWidget 렌더링:", {
// component,
// componentConfig: (component as any).componentConfig,
// tabs,
// tabsLength: tabs.length
// });
const [activeTab, setActiveTab] = useState<string>(defaultTab || tabs[0]?.id || "");
const [loadedScreens, setLoadedScreens] = useState<Record<string, any>>({});
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
// 탭 변경 시 화면 로드
useEffect(() => {
if (!activeTab) return;
const currentTab = tabs.find((tab) => tab.id === activeTab);
if (!currentTab || !currentTab.screenId) return;
// 이미 로드된 화면이면 스킵
if (loadedScreens[activeTab]) return;
// 이미 로딩 중이면 스킵
if (loadingScreens[activeTab]) return;
// 화면 로드 시작
loadScreen(activeTab, currentTab.screenId);
}, [activeTab, tabs]);
const loadScreen = async (tabId: string, screenId: number) => {
setLoadingScreens((prev) => ({ ...prev, [tabId]: true }));
setScreenErrors((prev) => ({ ...prev, [tabId]: "" }));
try {
const layoutData = await screenApi.getLayout(screenId);
if (layoutData) {
setLoadedScreens((prev) => ({
...prev,
[tabId]: {
screenId,
layout: layoutData,
},
}));
} else {
setScreenErrors((prev) => ({
...prev,
[tabId]: "화면을 불러올 수 없습니다",
}));
}
} catch (error: any) {
setScreenErrors((prev) => ({
...prev,
[tabId]: error.message || "화면 로드 중 오류가 발생했습니다",
}));
} finally {
setLoadingScreens((prev) => ({ ...prev, [tabId]: false }));
}
};
// 탭 콘텐츠 렌더링
const renderTabContent = (tab: TabItem) => {
const isLoading = loadingScreens[tab.id];
const error = screenErrors[tab.id];
const screenData = loadedScreens[tab.id];
// 로딩 중
if (isLoading) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground text-sm"> ...</p>
</div>
);
}
// 에러 발생
if (error) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="h-12 w-12 text-destructive" />
<div className="text-center">
<p className="mb-2 font-medium text-destructive"> </p>
<p className="text-muted-foreground text-sm">{error}</p>
</div>
</div>
);
}
// 화면 ID가 없는 경우
if (!tab.screenId) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground mb-2 text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</div>
);
}
// 화면 렌더링 - 원본 화면의 모든 컴포넌트를 그대로 렌더링
if (screenData && screenData.layout && screenData.layout.components) {
const components = screenData.layout.components;
const screenResolution = screenData.layout.screenResolution || { width: 1920, height: 1080 };
return (
<div className="bg-white" style={{ width: `${screenResolution.width}px`, height: '100%' }}>
<div className="relative h-full">
{components.map((comp) => (
<InteractiveScreenViewerDynamic
key={comp.id}
component={comp}
allComponents={components}
screenInfo={{ id: tab.screenId }}
/>
))}
</div>
</div>
);
}
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
</div>
);
};
// 빈 탭 목록
if (tabs.length === 0) {
return (
<Card className="flex h-full w-full items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</Card>
);
}
return (
<div className="h-full w-full overflow-auto">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
orientation={orientation}
className="flex h-full w-full flex-col"
>
<TabsList className={orientation === "horizontal" ? "justify-start shrink-0" : "flex-col shrink-0"}>
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
disabled={tab.disabled}
className={orientation === "horizontal" ? "" : "w-full justify-start"}
>
<span>{tab.label}</span>
{tab.screenName && (
<Badge variant="secondary" className="ml-2 text-[10px]">
{tab.screenName}
</Badge>
)}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent
key={tab.id}
value={tab.id}
className="flex-1 mt-0 data-[state=inactive]:hidden"
>
{renderTabContent(tab)}
</TabsContent>
))}
</Tabs>
</div>
);
};