diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 5fd3dcfd..5438b0c0 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -161,13 +161,24 @@ interface DashboardViewerProps { */ export function DashboardViewer({ elements, - dashboardId, refreshInterval, backgroundColor = "#f9fafb", resolution = "fhd", }: DashboardViewerProps) { const [elementData, setElementData] = useState>({}); const [loadingElements, setLoadingElements] = useState>(new Set()); + const [isMobile, setIsMobile] = useState(false); + + // 화면 크기 감지 + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 1024); // 1024px (lg) 미만은 모바일/태블릿 + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); + }, []); // 캔버스 설정 계산 const canvasConfig = useMemo(() => { @@ -269,6 +280,21 @@ export function DashboardViewer({ return () => clearInterval(interval); }, [refreshInterval, loadAllData]); + // 모바일에서 요소를 자연스러운 읽기 순서로 정렬 (왼쪽→오른쪽, 위→아래) + const sortedElements = useMemo(() => { + if (!isMobile) return elements; + + return [...elements].sort((a, b) => { + // Y 좌표 차이가 50px 이상이면 Y 우선 (같은 행으로 간주 안함) + const yDiff = a.position.y - b.position.y; + if (Math.abs(yDiff) > 50) { + return yDiff; + } + // 같은 행이면 X 좌표로 정렬 + return a.position.x - b.position.x; + }); + }, [elements, isMobile]); + // 요소가 없는 경우 if (elements.length === 0) { return ( @@ -284,30 +310,47 @@ export function DashboardViewer({ return ( - {/* 스크롤 가능한 컨테이너 */} -
- {/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */} -
- {/* 대시보드 요소들 */} - {elements.map((element) => ( - loadElementData(element)} - /> - ))} + {isMobile ? ( + // 모바일/태블릿: 세로 스택 레이아웃 +
+
+ {sortedElements.map((element) => ( + loadElementData(element)} + isMobile={true} + /> + ))} +
-
+ ) : ( + // 데스크톱: 기존 고정 캔버스 레이아웃 +
+
+ {sortedElements.map((element) => ( + loadElementData(element)} + isMobile={false} + /> + ))} +
+
+ )} ); } @@ -317,14 +360,61 @@ interface ViewerElementProps { data?: QueryResult; isLoading: boolean; onRefresh: () => void; + isMobile: boolean; } /** * 개별 뷰어 요소 컴포넌트 */ -function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementProps) { +function ViewerElement({ element, data, isLoading, onRefresh, isMobile }: ViewerElementProps) { const [isHovered, setIsHovered] = useState(false); + if (isMobile) { + // 모바일/태블릿: 세로 스택 카드 스타일 + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {element.showHeader !== false && ( +
+

{element.customTitle || element.title}

+ +
+ )} +
+ {element.type === "chart" ? ( + + ) : ( + renderWidget(element) + )} +
+ {isLoading && ( +
+
+
+
업데이트 중...
+
+
+ )} +
+ ); + } + + // 데스크톱: 기존 absolute positioning return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {/* 헤더 (showHeader가 false가 아닐 때만 표시) */} {element.showHeader !== false && (

{element.customTitle || element.title}

- - {/* 새로고침 버튼 (항상 렌더링하되 opacity로 제어) */}
)} - - {/* 내용 */}
{element.type === "chart" ? ( @@ -368,13 +453,11 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro renderWidget(element) )}
- - {/* 로딩 오버레이 */} {isLoading && (
-
-
업데이트 중...
+
+
업데이트 중...
)}