diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 0620b783..b8ec7db5 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -122,18 +122,27 @@ export default function PopRenderer({ return Math.max(10, maxRowEnd + 3); }, [components, overrides, mode, hiddenIds]); - // CSS Grid 스타일 (행 높이 강제 고정: 셀 크기 = 컴포넌트 크기의 기준) + // CSS Grid 스타일 + // 디자인 모드: 행 높이 고정 (정밀한 레이아웃 편집) + // 뷰어 모드: minmax(rowHeight, auto) (컴포넌트가 컨텐츠에 맞게 확장 가능) + const rowTemplate = isDesignMode + ? `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)` + : `repeat(${dynamicRowCount}, minmax(${breakpoint.rowHeight}px, auto))`; + const autoRowHeight = isDesignMode + ? `${breakpoint.rowHeight}px` + : `minmax(${breakpoint.rowHeight}px, auto)`; + const gridStyle = useMemo((): React.CSSProperties => ({ display: "grid", gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`, - gridTemplateRows: `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)`, - gridAutoRows: `${breakpoint.rowHeight}px`, + gridTemplateRows: rowTemplate, + gridAutoRows: autoRowHeight, gap: `${finalGap}px`, padding: `${finalPadding}px`, minHeight: "100%", backgroundColor: "#ffffff", position: "relative", - }), [breakpoint, finalGap, finalPadding, dynamicRowCount]); + }), [breakpoint, finalGap, finalPadding, dynamicRowCount, rowTemplate, autoRowHeight]); // 그리드 가이드 셀 생성 (동적 행 수) const gridCells = useMemo(() => { @@ -265,11 +274,11 @@ export default function PopRenderer({ ); } - // 뷰어 모드: 드래그 없는 일반 렌더링 + // 뷰어 모드: 드래그 없는 일반 렌더링 (overflow visible로 컨텐츠 확장 허용) return (
+
); diff --git a/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx b/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx index 7f75359a..08d4fadf 100644 --- a/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-string-list/PopStringListComponent.tsx @@ -5,11 +5,11 @@ * * 리스트 모드: 엑셀형 행/열 (CSS Grid) * 카드 모드: 셀 병합 가능한 카드 (CSS Grid + colSpan/rowSpan) - * 오버플로우: visibleRows 제한 + "전체보기" 확장 + * 오버플로우: visibleRows 제한 + "더보기" 점진 확장 */ import { useState, useEffect, useCallback, useMemo } from "react"; -import { ChevronDown, ChevronUp, Loader2, AlertCircle, ChevronsUpDown } from "lucide-react"; +import { ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Loader2, AlertCircle, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Popover, @@ -70,7 +70,10 @@ export function PopStringListComponent({ const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [expanded, setExpanded] = useState(false); + // 더보기 모드: 현재 표시 중인 행 수 + const [displayCount, setDisplayCount] = useState(0); + // 페이지네이션 모드: 현재 페이지 (1부터 시작) + const [currentPage, setCurrentPage] = useState(1); // 카드 버튼 행 단위 로딩 인덱스 (-1 = 로딩 없음) const [loadingRowIdx, setLoadingRowIdx] = useState(-1); @@ -112,21 +115,54 @@ export function PopStringListComponent({ [rows, publish, screenId] ); - // 오버플로우 계산 (JSON 복원 시 string 유입 방어) + // 오버플로우 설정 (JSON 복원 시 string 유입 방어) + const overflowMode = overflow?.mode || "loadMore"; const visibleRows = Number(overflow?.visibleRows) || 5; - const maxExpandRows = Number(overflow?.maxExpandRows) || 20; + const loadMoreCount = Number(overflow?.loadMoreCount) || 5; + const maxExpandRows = Number(overflow?.maxExpandRows) || 50; const showExpandButton = overflow?.showExpandButton ?? true; + const pageSize = Number(overflow?.pageSize) || visibleRows; + const paginationStyle = overflow?.paginationStyle || "bottom"; - // 표시할 데이터 슬라이스 - const visibleData = expanded - ? rows.slice(0, maxExpandRows) - : rows.slice(0, visibleRows); - const hasMore = rows.length > visibleRows; + // --- 더보기 모드 --- + useEffect(() => { + setDisplayCount(visibleRows); + }, [visibleRows]); - // 확장/축소 토글 - const toggleExpanded = useCallback(() => { - setExpanded((prev) => !prev); - }, []); + const effectiveLimit = Math.min(displayCount || visibleRows, maxExpandRows, rows.length); + const hasMore = showExpandButton && rows.length > effectiveLimit && effectiveLimit < maxExpandRows; + const isExpanded = effectiveLimit > visibleRows; + + const handleLoadMore = useCallback(() => { + setDisplayCount((prev) => { + const current = prev || visibleRows; + return Math.min(current + loadMoreCount, maxExpandRows, rows.length); + }); + }, [visibleRows, loadMoreCount, maxExpandRows, rows.length]); + + const handleCollapse = useCallback(() => { + setDisplayCount(visibleRows); + }, [visibleRows]); + + // --- 페이지네이션 모드 --- + const totalPages = Math.max(1, Math.ceil(rows.length / pageSize)); + + useEffect(() => { + setCurrentPage(1); + }, [pageSize, rows.length]); + + const handlePageChange = useCallback((page: number) => { + setCurrentPage(Math.max(1, Math.min(page, totalPages))); + }, [totalPages]); + + // --- 모드별 visibleData 결정 --- + const visibleData = useMemo(() => { + if (overflowMode === "pagination") { + const start = (currentPage - 1) * pageSize; + return rows.slice(start, start + pageSize); + } + return rows.slice(0, effectiveLimit); + }, [overflowMode, rows, currentPage, pageSize, effectiveLimit]); // dataSource 원시값 추출 (객체 참조 대신 안정적인 의존성 사용) const dsTableName = dataSource?.tableName; @@ -236,8 +272,10 @@ export function PopStringListComponent({ ); } + const isPaginationSide = overflowMode === "pagination" && paginationStyle === "side" && totalPages > 1; + return ( -
+
{/* 헤더 */} {header?.enabled && header.label && (
@@ -246,12 +284,9 @@ export function PopStringListComponent({ )} {/* 컨텐츠 */} -
+
{displayMode === "list" ? ( - + ) : ( )} + {isPaginationSide && ( + <> + + + + )}
- {/* 전체보기 버튼 */} - {showExpandButton && hasMore && ( -
- + {/* side 모드 페이지 인디케이터 (컨텐츠 아래 별도 영역) */} + {isPaginationSide && ( +
+ + {currentPage} / {totalPages} + +
+ )} + + {/* 더보기 모드 컨트롤 */} + {overflowMode === "loadMore" && showExpandButton && (hasMore || isExpanded) && ( +
+ {hasMore && ( + + )} + {isExpanded && ( + + )} +
+ )} + + {/* 페이지네이션 bottom 모드 컨트롤 */} + {overflowMode === "pagination" && paginationStyle === "bottom" && totalPages > 1 && ( +
+ + {rows.length}건 중 {(currentPage - 1) * pageSize + 1}-{Math.min(currentPage * pageSize, rows.length)} + +
+ + + {currentPage} / {totalPages} + + +
)}
diff --git a/frontend/lib/registry/pop-components/pop-string-list/PopStringListConfig.tsx b/frontend/lib/registry/pop-components/pop-string-list/PopStringListConfig.tsx index f4702a16..4208301b 100644 --- a/frontend/lib/registry/pop-components/pop-string-list/PopStringListConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-string-list/PopStringListConfig.tsx @@ -67,7 +67,7 @@ interface ConfigPanelProps { const DEFAULT_CONFIG: PopStringListConfig = { displayMode: "list", header: { enabled: true, label: "" }, - overflow: { visibleRows: 5, showExpandButton: true, maxExpandRows: 20 }, + overflow: { visibleRows: 5, mode: "loadMore", showExpandButton: true, loadMoreCount: 5, maxExpandRows: 50, pageSize: 5, paginationStyle: "bottom" }, dataSource: { tableName: "" }, listColumns: [], cardGrid: undefined, @@ -414,6 +414,8 @@ function StepOverflow({ overflow: PopStringListConfig["overflow"]; onChange: (overflow: PopStringListConfig["overflow"]) => void; }) { + const mode = overflow.mode || "loadMore"; + return (
@@ -429,32 +431,130 @@ function StepOverflow({ className="mt-1 h-8 text-xs" />
-
- - - onChange({ ...overflow, showExpandButton }) + +
+ +
- {overflow.showExpandButton && ( -
- - - onChange({ - ...overflow, - maxExpandRows: Number(e.target.value) || 20, - }) - } - className="mt-1 h-8 text-xs" - /> -
+ + {mode === "loadMore" && ( + <> +
+ + + onChange({ ...overflow, showExpandButton }) + } + /> +
+ {overflow.showExpandButton && ( + <> +
+ + + onChange({ + ...overflow, + loadMoreCount: Number(e.target.value) || 5, + }) + } + className="mt-1 h-8 text-xs" + /> +

+ 클릭할 때마다 추가로 표시할 행 수 +

+
+
+ + + onChange({ + ...overflow, + maxExpandRows: Number(e.target.value) || 50, + }) + } + className="mt-1 h-8 text-xs" + /> +
+ + )} + + )} + + {mode === "pagination" && ( + <> +
+ + + onChange({ + ...overflow, + pageSize: Number(e.target.value) || 5, + }) + } + className="mt-1 h-8 text-xs" + /> +
+
+ + +

+ {(overflow.paginationStyle || "bottom") === "bottom" + ? "컴포넌트 하단에 이전/다음 버튼과 페이지 번호 표시" + : "컴포넌트 좌우에 화살표 버튼 표시"} +

+
+ )}
); diff --git a/frontend/lib/registry/pop-components/pop-string-list/index.tsx b/frontend/lib/registry/pop-components/pop-string-list/index.tsx index 3d148c57..86fa156d 100644 --- a/frontend/lib/registry/pop-components/pop-string-list/index.tsx +++ b/frontend/lib/registry/pop-components/pop-string-list/index.tsx @@ -16,7 +16,7 @@ import type { PopStringListConfig } from "./types"; const defaultConfig: PopStringListConfig = { displayMode: "list", header: { enabled: true, label: "" }, - overflow: { visibleRows: 5, showExpandButton: true, maxExpandRows: 20 }, + overflow: { visibleRows: 5, mode: "loadMore", showExpandButton: true, loadMoreCount: 5, maxExpandRows: 50, pageSize: 5, paginationStyle: "bottom" }, dataSource: { tableName: "" }, listColumns: [], cardGrid: undefined, diff --git a/frontend/lib/registry/pop-components/pop-string-list/types.ts b/frontend/lib/registry/pop-components/pop-string-list/types.ts index 3fe0d748..6679cb54 100644 --- a/frontend/lib/registry/pop-components/pop-string-list/types.ts +++ b/frontend/lib/registry/pop-components/pop-string-list/types.ts @@ -47,11 +47,23 @@ export interface ListColumnConfig { alternateColumns?: string[]; // 런타임에서 전환 가능한 대체 컬럼 목록 } +/** 오버플로우 방식 */ +export type OverflowMode = "loadMore" | "pagination"; + +/** 페이지네이션 네비게이션 스타일 */ +export type PaginationStyle = "bottom" | "side"; + /** 오버플로우 설정 */ export interface OverflowConfig { visibleRows: number; // 기본 표시 행 수 - showExpandButton: boolean; // "전체보기" 버튼 표시 - maxExpandRows: number; // 확장 시 최대 행 수 + mode: OverflowMode; // 오버플로우 방식 + // 더보기 모드 전용 + showExpandButton: boolean; // "더보기" 버튼 표시 + loadMoreCount: number; // 더보기 클릭 시 추가 로딩 행 수 + maxExpandRows: number; // 최대 표시 행 수 (무한 확장 방지) + // 페이지네이션 모드 전용 + pageSize: number; // 페이지당 표시 행 수 + paginationStyle: PaginationStyle; // bottom: 하단 페이지 표시, side: 좌우 화살표 } /** 헤더 설정 */