From 79c1a456f0315a9b6cf0a5c87eddd831ed3b5b6c Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 18 Dec 2025 15:04:55 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9C=84?= =?UTF-8?q?=EC=A0=AF=20=EC=BB=B4=ED=8C=A9=ED=8A=B8=20=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=EC=84=B8=EB=A1=9C=201=EC=B9=B8=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/widgets/ListWidget.tsx | 147 ++++++++--- .../dashboard/widgets/ListTestWidget.tsx | 233 ++++++++++++------ 2 files changed, 263 insertions(+), 117 deletions(-) diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx index 194f7210..2f05a927 100644 --- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx @@ -14,7 +14,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { getApiUrl } from "@/lib/utils/apiUrl"; -import { Truck, Clock, MapPin, Package, Info } from "lucide-react"; +import { Truck, Clock, MapPin, Package, Info, ChevronLeft, ChevronRight } from "lucide-react"; interface ListWidgetProps { element: DashboardElement; @@ -32,6 +32,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); + const [containerHeight, setContainerHeight] = useState(0); + const containerRef = React.useRef(null); // 행 상세 팝업 상태 const [detailPopupOpen, setDetailPopupOpen] = useState(false); @@ -39,6 +41,25 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const [detailPopupLoading, setDetailPopupLoading] = useState(false); const [additionalDetailData, setAdditionalDetailData] = useState | null>(null); + // 컨테이너 높이 감지 + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerHeight(entry.contentRect.height); + } + }); + + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, []); + + // 컴팩트 모드 여부 (높이 300px 이하 또는 element 높이가 300px 이하) + const elementHeight = element?.size?.height || 0; + const isCompactHeight = elementHeight > 0 ? elementHeight < 300 : (containerHeight > 0 && containerHeight < 300); + const config = element.listConfig || { columnMode: "auto", viewMode: "table", @@ -541,14 +562,64 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows; return ( -
- {/* 제목 - 항상 표시 */} -
-

{element.customTitle || element.title}

-
+
+ {/* 컴팩트 모드 (세로 1칸) - 캐러셀 형태로 한 건씩 표시 */} + {isCompactHeight ? ( +
+ {data && data.rows.length > 0 && displayColumns.filter((col) => col.visible).length > 0 ? ( +
+ {/* 이전 버튼 */} + - {/* 테이블 뷰 */} - {config.viewMode === "table" && ( + {/* 현재 데이터 */} +
+ {displayColumns.filter((col) => col.visible).slice(0, 4).map((col, colIdx) => ( + + {String(data.rows[currentPage - 1]?.[col.dataKey || col.field] ?? "").substring(0, 25)} + {colIdx < Math.min(displayColumns.filter((c) => c.visible).length, 4) - 1 && " | "} + + ))} +
+ + {/* 다음 버튼 */} + +
+ ) : ( +
데이터 없음
+ )} + + {/* 현재 위치 표시 (작게) */} + {data && data.rows.length > 0 && ( +
+ {currentPage} / {data.rows.length} +
+ )} +
+ ) : ( + <> + {/* 제목 - 항상 표시 */} +
+

{element.customTitle || element.title}

+
+ + {/* 테이블 뷰 */} + {config.viewMode === "table" && (
{config.showHeader && ( @@ -642,36 +713,38 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { )} - {/* 페이지네이션 */} - {config.enablePagination && totalPages > 1 && ( -
-
- {startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개 -
-
- -
- {currentPage} - / - {totalPages} + {/* 페이지네이션 */} + {config.enablePagination && totalPages > 1 && ( +
+
+ {startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개 +
+
+ +
+ {currentPage} + / + {totalPages} +
+ +
- -
-
+ )} + )} {/* 행 상세 팝업 */} diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index d1303d10..a1609b1a 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -13,7 +13,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Loader2, RefreshCw, Truck, Clock, MapPin, Package, Info } from "lucide-react"; +import { Loader2, RefreshCw, Truck, Clock, MapPin, Package, Info, ChevronLeft, ChevronRight } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; import { getApiUrl } from "@/lib/utils/apiUrl"; @@ -41,6 +41,8 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [lastRefreshTime, setLastRefreshTime] = useState(null); + const [containerHeight, setContainerHeight] = useState(0); + const containerRef = React.useRef(null); // 행 상세 팝업 상태 const [detailPopupOpen, setDetailPopupOpen] = useState(false); @@ -48,6 +50,25 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [detailPopupLoading, setDetailPopupLoading] = useState(false); const [additionalDetailData, setAdditionalDetailData] = useState | null>(null); + // 컨테이너 높이 감지 + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerHeight(entry.contentRect.height); + } + }); + + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, []); + + // 컴팩트 모드 여부 (높이 300px 이하 또는 element 높이가 300px 이하) + const elementHeight = element?.size?.height || 0; + const isCompactHeight = elementHeight > 0 ? elementHeight < 300 : (containerHeight > 0 && containerHeight < 300); + // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { @@ -743,87 +764,139 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { }; return ( -
- {/* 헤더 */} -
-
-

- {element?.customTitle || "리스트"} -

-

- {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행 - {lastRefreshTime && ( - - • {lastRefreshTime.toLocaleTimeString("ko-KR")} - +

+ {/* 컴팩트 모드 (세로 1칸) - 캐러셀 형태로 한 건씩 표시 */} + {isCompactHeight ? ( +
+ {data && data.rows.length > 0 && displayColumns.length > 0 ? ( +
+ {/* 이전 버튼 */} + + + {/* 현재 데이터 */} +
+ {displayColumns.slice(0, 4).map((field, fieldIdx) => ( + + {String(data.rows[currentPage - 1]?.[field] ?? "").substring(0, 25)} + {fieldIdx < Math.min(displayColumns.length, 4) - 1 && " | "} + + ))} +
+ + {/* 다음 버튼 */} + +
+ ) : ( +
데이터 없음
+ )} + + {/* 현재 위치 표시 (작게) */} + {data && data.rows.length > 0 && ( +
+ {currentPage} / {data.rows.length} +
+ )} +
+ ) : ( + <> + {/* 헤더 */} +
+
+

+ {element?.customTitle || "리스트"} +

+

+ {dataSources?.length || 0}개 데이터 소스 • {data?.totalRows || 0}개 행 + {lastRefreshTime && ( + + • {lastRefreshTime.toLocaleTimeString("ko-KR")} + + )} +

+
+
+ + {isLoading && } +
+
+ + {/* 컨텐츠 */} +
+ {error ? ( +
+

{error}

+
+ ) : !dataSources || dataSources.length === 0 ? ( +
+

+ 데이터 소스를 연결해주세요 +

+
+ ) : !data || data.rows.length === 0 ? ( +
+

+ 데이터가 없습니다 +

+
+ ) : config.viewMode === "card" ? ( + renderCards() + ) : ( + renderTable() )} -

-
-
- - {isLoading && } -
-
+
- {/* 컨텐츠 */} -
- {error ? ( -
-

{error}

-
- ) : !dataSources || dataSources.length === 0 ? ( -
-

- 데이터 소스를 연결해주세요 -

-
- ) : !data || data.rows.length === 0 ? ( -
-

- 데이터가 없습니다 -

-
- ) : config.viewMode === "card" ? ( - renderCards() - ) : ( - renderTable() - )} -
- - {/* 페이지네이션 */} - {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && ( -
-
- 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages}) -
-
- - -
-
+ {/* 페이지네이션 */} + {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && ( +
+
+ 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages}) +
+
+ + +
+
+ )} + )} {/* 행 상세 팝업 */}