From 12a8290873bca77773e1d5d1b7040778a0f5c956 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 5 Mar 2026 18:54:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(pop):=20=EC=84=A4=EC=A0=95=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=95=84=EC=BD=94=EB=94=94=EC=96=B8=20=EC=A0=91?= =?UTF-8?q?=EA=B8=B0/=ED=8E=BC=EC=B9=98=EA=B8=B0=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=84=B1=20+=20sessionStorage=20=EC=83=81=ED=83=9C=20=EA=B8=B0?= =?UTF-8?q?=EC=96=B5=20=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90=EC=9D=84=20?= =?UTF-8?q?=EC=97=B4=20=EB=95=8C=20=EC=84=B9=EC=85=98=EC=9D=B4=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=EB=8A=94=20=ED=8E=BC=EC=B3=90=EC=A0=B8=20=EC=9E=88?= =?UTF-8?q?=EA=B3=A0=20=EC=9D=BC=EB=B6=80=EB=8A=94=20=EC=A0=91=ED=98=80=20?= =?UTF-8?q?=EC=9E=88=EC=96=B4=20=EC=9D=BC=EA=B4=80=EC=84=B1=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8D=98=20UX=EB=A5=BC=20=EA=B0=9C=EC=84=A0=ED=95=98?= =?UTF-8?q?=EA=B3=A0,=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=ED=8E=BC?= =?UTF-8?q?=EC=B9=9C=20=EC=84=B9=EC=85=98=EC=9D=84=20=ED=83=AD=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=EB=82=B4=EC=97=90=EC=84=9C=20=EA=B8=B0=EC=96=B5?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20-=20useCollapsibleSections=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=83=9D=EC=84=B1=20(sessionS?= =?UTF-8?q?torage=20=EA=B8=B0=EB=B0=98,=20=EC=B4=88=EA=B8=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=A0=91=ED=9E=98)=20-=20PopCardListConfig:=20Coll?= =?UTF-8?q?apsibleSection=EC=97=90=20sectionKey/sections=20prop=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20-=20PopFieldConfig:?= =?UTF-8?q?=20SaveTabContent=205=EA=B0=9C=20=EA=B3=A0=EC=A0=95=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=ED=9B=85=20=EC=A0=81=EC=9A=A9,=20=20=20SectionEdit?= =?UTF-8?q?or=20=EC=B4=88=EA=B8=B0=EA=B0=92=20=EC=A0=91=ED=9E=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20PopDashboardConfig:=20Page?= =?UTF-8?q?Editor=20=EC=B4=88=EA=B8=B0=EA=B0=92=20=EC=A0=91=ED=9E=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/pop/index.ts | 3 + frontend/hooks/pop/useCollapsibleSections.ts | 58 +++++++++++++++++++ .../pop-card-list/PopCardListConfig.tsx | 50 +++++++++++----- .../pop-dashboard/PopDashboardConfig.tsx | 2 +- .../pop-field/PopFieldConfig.tsx | 58 +++++++++---------- 5 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 frontend/hooks/pop/useCollapsibleSections.ts diff --git a/frontend/hooks/pop/index.ts b/frontend/hooks/pop/index.ts index 7ae7e953..893f09e9 100644 --- a/frontend/hooks/pop/index.ts +++ b/frontend/hooks/pop/index.ts @@ -26,5 +26,8 @@ export { useConnectionResolver } from "./useConnectionResolver"; export { useCartSync } from "./useCartSync"; export type { UseCartSyncReturn } from "./useCartSync"; +// 설정 패널 접기/펼치기 상태 관리 +export { useCollapsibleSections } from "./useCollapsibleSections"; + // SQL 빌더 유틸 (고급 사용 시) export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder"; diff --git a/frontend/hooks/pop/useCollapsibleSections.ts b/frontend/hooks/pop/useCollapsibleSections.ts new file mode 100644 index 00000000..e5636d65 --- /dev/null +++ b/frontend/hooks/pop/useCollapsibleSections.ts @@ -0,0 +1,58 @@ +import { useState, useCallback, useRef } from "react"; + +/** + * 설정 패널 접기/펼치기 상태를 sessionStorage로 기억하는 훅 + * + * - 초기 상태: 모든 섹션 접힘 + * - 사용자가 펼친 섹션은 같은 탭 세션 내에서 기억 + * - 탭 닫으면 초기화 + * + * @param storageKey sessionStorage 키 (예: "pop-card-list") + */ +export function useCollapsibleSections(storageKey: string) { + const fullKey = `pop-config-sections-${storageKey}`; + + const [openSections, setOpenSections] = useState>(() => { + if (typeof window === "undefined") return new Set(); + try { + const saved = sessionStorage.getItem(fullKey); + if (saved) return new Set(JSON.parse(saved)); + } catch {} + return new Set(); + }); + + const openSectionsRef = useRef(openSections); + openSectionsRef.current = openSections; + + const persist = useCallback( + (next: Set) => { + try { + sessionStorage.setItem(fullKey, JSON.stringify([...next])); + } catch {} + }, + [fullKey], + ); + + const isOpen = useCallback( + (key: string) => openSectionsRef.current.has(key), + [], + ); + + const toggle = useCallback( + (key: string) => { + setOpenSections((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + persist(next); + return next; + }); + }, + [persist], + ); + + return { isOpen, toggle }; +} diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx index 106ad796..6383974b 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx @@ -10,6 +10,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { ChevronDown, ChevronRight, Plus, Trash2, Database, Check } from "lucide-react"; +import { useCollapsibleSections } from "@/hooks/pop/useCollapsibleSections"; import type { GridMode } from "@/components/pop/designer/types/pop-layout"; import { GRID_BREAKPOINTS } from "@/components/pop/designer/types/pop-layout"; import { Button } from "@/components/ui/button"; @@ -135,6 +136,7 @@ const COLOR_OPTIONS = [ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentColSpan }: ConfigPanelProps) { const [activeTab, setActiveTab] = useState<"basic" | "template">("basic"); + const sections = useCollapsibleSections("pop-card-list"); const cfg: PopCardListConfig = config || DEFAULT_CONFIG; @@ -184,6 +186,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC onUpdate={updateConfig} currentMode={currentMode} currentColSpan={currentColSpan} + sections={sections} /> )} {activeTab === "template" && ( @@ -195,7 +198,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC ) : ( - + ) )} @@ -205,16 +208,20 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC // ===== 기본 설정 탭 (테이블 + 레이아웃 통합) ===== +type SectionsApi = { isOpen: (key: string) => boolean; toggle: (key: string) => void }; + function BasicSettingsTab({ config, onUpdate, currentMode, currentColSpan, + sections, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; currentMode?: GridMode; currentColSpan?: number; + sections: SectionsApi; }) { const dataSource = config.dataSource || DEFAULT_DATA_SOURCE; const [tables, setTables] = useState([]); @@ -321,7 +328,7 @@ function BasicSettingsTab({ return (
{/* 장바구니 목록 모드 */} - + onUpdate({ cartListMode })} @@ -330,7 +337,7 @@ function BasicSettingsTab({ {/* 테이블 선택 (장바구니 모드 시 숨김) */} {!isCartListMode && ( - +
@@ -365,7 +372,9 @@ function BasicSettingsTab({ {/* 조인 설정 (장바구니 모드 시 숨김) */} {!isCartListMode && dataSource.tableName && ( 0 ? `${dataSource.joins.length}개` @@ -383,7 +392,9 @@ function BasicSettingsTab({ {/* 정렬 기준 (장바구니 모드 시 숨김) */} {!isCartListMode && dataSource.tableName && ( 0 ? `${dataSource.filters.length}개` @@ -421,7 +434,9 @@ function BasicSettingsTab({ {/* 저장 매핑 (장바구니 모드일 때만) */} {isCartListMode && ( 0 ? `${config.saveMapping.mappings.length}개` @@ -437,7 +452,7 @@ function BasicSettingsTab({ )} {/* 레이아웃 설정 */} - +
{modeLabel && (
@@ -526,9 +541,11 @@ function BasicSettingsTab({ function CardTemplateTab({ config, onUpdate, + sections, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; + sections: SectionsApi; }) { const dataSource = config.dataSource || DEFAULT_DATA_SOURCE; const template = config.cardTemplate || DEFAULT_TEMPLATE; @@ -634,7 +651,7 @@ function CardTemplateTab({ return (
{/* 헤더 설정 */} - + {/* 이미지 설정 */} - + {/* 입력 필드 설정 */} - + {/* 포장등록 설정 */} - + onUpdate({ packageConfig })} @@ -683,7 +701,7 @@ function CardTemplateTab({ {/* 담기 버튼 설정 */} - + onUpdate({ cartAction })} @@ -693,7 +711,7 @@ function CardTemplateTab({ {/* 반응형 표시 설정 */} - +