From b30e3480d4a62818849f76c6b9d370c00ac2ad66 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 11:36:25 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/DashboardViewer.tsx | 152 ++++++++++++++---- 1 file changed, 118 insertions(+), 34 deletions(-) diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 5fd3dcfd..3172d2ad 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -168,6 +168,18 @@ export function DashboardViewer({ }: 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(() => { @@ -282,32 +294,64 @@ export function DashboardViewer({ ); } + // 모바일에서 요소를 자연스러운 읽기 순서로 정렬 (왼쪽→오른쪽, 위→아래) + 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]); + return ( - {/* 스크롤 가능한 컨테이너 */} -
- {/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */} -
- {/* 대시보드 요소들 */} - {elements.map((element) => ( - loadElementData(element)} - /> - ))} + {isMobile ? ( + // 모바일/태블릿: 세로 스택 레이아웃 +
+
+ {sortedElements.map((element) => ( + loadElementData(element)} + isMobile={true} + /> + ))} +
-
+ ) : ( + // 데스크톱: 기존 고정 캔버스 레이아웃 +
+
+ {sortedElements.map((element) => ( + loadElementData(element)} + isMobile={false} + /> + ))} +
+
+ )} ); } @@ -317,14 +361,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 +454,11 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro renderWidget(element) )}
- - {/* 로딩 오버레이 */} {isLoading && (
-
-
업데이트 중...
+
+
업데이트 중...
)} -- 2.43.0 From f088a2d995ae27225bb5b25cc2ea8c2a5feb5b93 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 21 Oct 2025 13:10:03 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=B7=B0=EC=96=B4=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/DashboardViewer.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 3172d2ad..5438b0c0 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -161,7 +161,6 @@ interface DashboardViewerProps { */ export function DashboardViewer({ elements, - dashboardId, refreshInterval, backgroundColor = "#f9fafb", resolution = "fhd", @@ -281,19 +280,6 @@ export function DashboardViewer({ return () => clearInterval(interval); }, [refreshInterval, loadAllData]); - // 요소가 없는 경우 - if (elements.length === 0) { - return ( -
-
-
📊
-
표시할 요소가 없습니다
-
대시보드 편집기에서 차트나 위젯을 추가해보세요
-
-
- ); - } - // 모바일에서 요소를 자연스러운 읽기 순서로 정렬 (왼쪽→오른쪽, 위→아래) const sortedElements = useMemo(() => { if (!isMobile) return elements; @@ -309,6 +295,19 @@ export function DashboardViewer({ }); }, [elements, isMobile]); + // 요소가 없는 경우 + if (elements.length === 0) { + return ( +
+
+
📊
+
표시할 요소가 없습니다
+
대시보드 편집기에서 차트나 위젯을 추가해보세요
+
+
+ ); + } + return ( {isMobile ? ( -- 2.43.0