Compare commits

..

No commits in common. "12a8290873bca77773e1d5d1b7040778a0f5c956" and "85bf4882a8ea7c4da5eb40009a1a83a1123e4cd7" have entirely different histories.

6 changed files with 54 additions and 164 deletions

View File

@ -26,8 +26,5 @@ export { useConnectionResolver } from "./useConnectionResolver";
export { useCartSync } from "./useCartSync";
export type { UseCartSyncReturn } from "./useCartSync";
// 설정 패널 접기/펼치기 상태 관리
export { useCollapsibleSections } from "./useCollapsibleSections";
// SQL 빌더 유틸 (고급 사용 시)
export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder";

View File

@ -1,58 +0,0 @@
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<Set<string>>(() => {
if (typeof window === "undefined") return new Set<string>();
try {
const saved = sessionStorage.getItem(fullKey);
if (saved) return new Set<string>(JSON.parse(saved));
} catch {}
return new Set<string>();
});
const openSectionsRef = useRef(openSections);
openSectionsRef.current = openSections;
const persist = useCallback(
(next: Set<string>) => {
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 };
}

View File

@ -730,13 +730,14 @@ export function PopCardListComponent({
gap: `${scaled.gap}px`,
...(isHorizontalMode
? {
gridTemplateRows: `repeat(${gridRows}, minmax(${scaled.cardHeight}px, auto))`,
gridTemplateRows: `repeat(${gridRows}, ${scaled.cardHeight}px)`,
gridAutoFlow: "column",
gridAutoColumns: `${scaled.cardWidth}px`,
}
: {
// 세로 모드: 1fr 비율 기반으로 컨테이너 너비 초과 방지
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
gridAutoRows: `minmax(${scaled.cardHeight}px, auto)`,
gridAutoRows: `${scaled.cardHeight}px`,
}),
};
@ -1007,10 +1008,9 @@ function Card({
}
}, [effectiveMax, inputField?.enabled, limitCol, isCartListMode]);
const hasPackageEntries = packageEntries.length > 0;
const cardStyle: React.CSSProperties = {
minHeight: `${scaled.cardHeight}px`,
height: `${scaled.cardHeight}px`,
overflow: "hidden",
};
const headerStyle: React.CSSProperties = {
@ -1116,7 +1116,7 @@ function Card({
return (
<div
className={`relative flex cursor-pointer flex-col rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${borderClass}`}
className={`relative cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${borderClass}`}
style={cardStyle}
onClick={handleCardClick}
role="button"
@ -1157,7 +1157,7 @@ function Card({
)}
{/* 본문 영역 */}
<div className="flex flex-1 overflow-hidden" style={bodyStyle}>
<div className="flex" style={bodyStyle}>
{/* 이미지 (왼쪽) */}
{image?.enabled && (
<div className="shrink-0">
@ -1199,7 +1199,7 @@ function Card({
{/* 오른쪽: 수량 버튼 + 담기/취소/삭제 버튼 */}
{(inputField?.enabled || cartAction || isCartListMode) && (
<div
className="ml-2 flex shrink-0 flex-col items-stretch justify-start gap-2"
className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2"
style={{ minWidth: "100px" }}
>
{/* 수량 버튼 (입력 필드 ON일 때만) */}
@ -1268,37 +1268,6 @@ function Card({
)}
</div>
{/* 포장 요약 바: 본문 아래에 표시 */}
{hasPackageEntries && (
<div className="border-t bg-emerald-50">
{packageEntries.map((entry, idx) => (
<div
key={idx}
className="flex items-center justify-between px-3 py-1.5"
>
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500 px-2 py-0.5 text-[10px] font-bold text-white">
</span>
<Package className="h-4 w-4 text-emerald-600" />
<span
className="font-medium text-emerald-700"
style={{ fontSize: `${scaled.bodyTextSize}px` }}
>
{entry.packageCount}{entry.unitLabel} x {entry.quantityPerUnit}
</span>
</div>
<span
className="font-bold text-emerald-700"
style={{ fontSize: `${scaled.bodyTextSize}px` }}
>
= {entry.totalQuantity.toLocaleString()}{inputField?.unit || "EA"}
</span>
</div>
))}
</div>
)}
{inputField?.enabled && (
<NumberInputModal
open={isModalOpen}

View File

@ -10,7 +10,6 @@
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";
@ -136,7 +135,6 @@ 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;
@ -186,7 +184,6 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
onUpdate={updateConfig}
currentMode={currentMode}
currentColSpan={currentColSpan}
sections={sections}
/>
)}
{activeTab === "template" && (
@ -198,7 +195,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
</div>
</div>
) : (
<CardTemplateTab config={cfg} onUpdate={updateConfig} sections={sections} />
<CardTemplateTab config={cfg} onUpdate={updateConfig} />
)
)}
</div>
@ -208,20 +205,16 @@ 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<PopCardListConfig>) => void;
currentMode?: GridMode;
currentColSpan?: number;
sections: SectionsApi;
}) {
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
const [tables, setTables] = useState<TableInfo[]>([]);
@ -328,7 +321,7 @@ function BasicSettingsTab({
return (
<div className="space-y-4">
{/* 장바구니 목록 모드 */}
<CollapsibleSection sectionKey="basic-cart-mode" title="장바구니 목록 모드" sections={sections}>
<CollapsibleSection title="장바구니 목록 모드" defaultOpen={isCartListMode}>
<CartListModeSection
cartListMode={config.cartListMode}
onUpdate={(cartListMode) => onUpdate({ cartListMode })}
@ -337,7 +330,7 @@ function BasicSettingsTab({
{/* 테이블 선택 (장바구니 모드 시 숨김) */}
{!isCartListMode && (
<CollapsibleSection sectionKey="basic-table" title="테이블 선택" sections={sections}>
<CollapsibleSection title="테이블 선택" defaultOpen>
<div className="space-y-3">
<div>
<Label className="text-[10px] text-muted-foreground"> </Label>
@ -372,9 +365,7 @@ function BasicSettingsTab({
{/* 조인 설정 (장바구니 모드 시 숨김) */}
{!isCartListMode && dataSource.tableName && (
<CollapsibleSection
sectionKey="basic-join"
title="조인 설정"
sections={sections}
badge={
dataSource.joins && dataSource.joins.length > 0
? `${dataSource.joins.length}`
@ -392,9 +383,7 @@ function BasicSettingsTab({
{/* 정렬 기준 (장바구니 모드 시 숨김) */}
{!isCartListMode && dataSource.tableName && (
<CollapsibleSection
sectionKey="basic-sort"
title="정렬 기준"
sections={sections}
badge={
dataSource.sort
? Array.isArray(dataSource.sort)
@ -414,9 +403,7 @@ function BasicSettingsTab({
{/* 필터 기준 (장바구니 모드 시 숨김) */}
{!isCartListMode && dataSource.tableName && (
<CollapsibleSection
sectionKey="basic-filter"
title="필터 기준"
sections={sections}
badge={
dataSource.filters && dataSource.filters.length > 0
? `${dataSource.filters.length}`
@ -434,9 +421,7 @@ function BasicSettingsTab({
{/* 저장 매핑 (장바구니 모드일 때만) */}
{isCartListMode && (
<CollapsibleSection
sectionKey="basic-save-mapping"
title="저장 매핑"
sections={sections}
badge={
config.saveMapping?.mappings && config.saveMapping.mappings.length > 0
? `${config.saveMapping.mappings.length}`
@ -452,7 +437,7 @@ function BasicSettingsTab({
)}
{/* 레이아웃 설정 */}
<CollapsibleSection sectionKey="basic-layout" title="레이아웃 설정" sections={sections}>
<CollapsibleSection title="레이아웃 설정" defaultOpen>
<div className="space-y-3">
{modeLabel && (
<div className="flex items-center gap-1.5 rounded-md bg-muted/50 px-2.5 py-1.5">
@ -541,11 +526,9 @@ function BasicSettingsTab({
function CardTemplateTab({
config,
onUpdate,
sections,
}: {
config: PopCardListConfig;
onUpdate: (partial: Partial<PopCardListConfig>) => void;
sections: SectionsApi;
}) {
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
const template = config.cardTemplate || DEFAULT_TEMPLATE;
@ -651,7 +634,7 @@ function CardTemplateTab({
return (
<div className="space-y-4">
{/* 헤더 설정 */}
<CollapsibleSection sectionKey="tpl-header" title="헤더 설정" sections={sections}>
<CollapsibleSection title="헤더 설정" defaultOpen>
<HeaderSettingsSection
header={template.header || DEFAULT_HEADER}
columnGroups={columnGroups}
@ -660,7 +643,7 @@ function CardTemplateTab({
</CollapsibleSection>
{/* 이미지 설정 */}
<CollapsibleSection sectionKey="tpl-image" title="이미지 설정" sections={sections}>
<CollapsibleSection title="이미지 설정" defaultOpen>
<ImageSettingsSection
image={template.image || DEFAULT_IMAGE}
columnGroups={columnGroups}
@ -670,10 +653,9 @@ function CardTemplateTab({
{/* 본문 필드 */}
<CollapsibleSection
sectionKey="tpl-body"
title="본문 필드"
sections={sections}
badge={`${template.body?.fields?.length || 0}`}
defaultOpen
>
<BodyFieldsSection
body={template.body || DEFAULT_BODY}
@ -683,7 +665,7 @@ function CardTemplateTab({
</CollapsibleSection>
{/* 입력 필드 설정 */}
<CollapsibleSection sectionKey="tpl-input" title="입력 필드" sections={sections}>
<CollapsibleSection title="입력 필드" defaultOpen={false}>
<InputFieldSettingsSection
inputField={config.inputField}
columns={columns}
@ -693,7 +675,7 @@ function CardTemplateTab({
</CollapsibleSection>
{/* 포장등록 설정 */}
<CollapsibleSection sectionKey="tpl-package" title="포장등록 (계산기)" sections={sections}>
<CollapsibleSection title="포장등록 (계산기)" defaultOpen={false}>
<PackageSettingsSection
packageConfig={config.packageConfig}
onUpdate={(packageConfig) => onUpdate({ packageConfig })}
@ -701,7 +683,7 @@ function CardTemplateTab({
</CollapsibleSection>
{/* 담기 버튼 설정 */}
<CollapsibleSection sectionKey="tpl-cart" title="담기 버튼" sections={sections}>
<CollapsibleSection title="담기 버튼" defaultOpen={false}>
<CartActionSettingsSection
cartAction={config.cartAction}
onUpdate={(cartAction) => onUpdate({ cartAction })}
@ -711,7 +693,7 @@ function CardTemplateTab({
</CollapsibleSection>
{/* 반응형 표시 설정 */}
<CollapsibleSection sectionKey="tpl-responsive" title="반응형 표시" sections={sections}>
<CollapsibleSection title="반응형 표시" defaultOpen={false}>
<ResponsiveDisplaySection
config={config}
onUpdate={onUpdate}
@ -787,26 +769,24 @@ function GroupedColumnSelect({
// ===== 접기/펴기 섹션 컴포넌트 =====
function CollapsibleSection({
sectionKey,
title,
badge,
sections,
defaultOpen = false,
children,
}: {
sectionKey: string;
title: string;
badge?: string;
sections: SectionsApi;
defaultOpen?: boolean;
children: React.ReactNode;
}) {
const open = sections.isOpen(sectionKey);
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-md border">
<button
type="button"
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50"
onClick={() => sections.toggle(sectionKey)}
onClick={() => setOpen(!open)}
>
<div className="flex items-center gap-2">
{open ? (

View File

@ -1937,7 +1937,7 @@ function PageEditor({
isPreviewing?: boolean;
onUpdateItem?: (updatedItem: DashboardItem) => void;
}) {
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(true);
return (
<div className="rounded-md border p-2">

View File

@ -9,7 +9,6 @@
*/
import { useState, useEffect, useCallback, useMemo } from "react";
import { useCollapsibleSections } from "@/hooks/pop/useCollapsibleSections";
import {
ChevronDown,
ChevronRight,
@ -649,7 +648,10 @@ function SaveTabContent({
const noFields = allFields.length === 0;
const sections = useCollapsibleSections("pop-field");
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const toggleSection = useCallback((key: string) => {
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
}, []);
return (
<div className="space-y-4">
@ -664,16 +666,16 @@ function SaveTabContent({
<div className="rounded-md border bg-card">
<div
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
onClick={() => sections.toggle("table")}
onClick={() => toggleSection("table")}
>
{sections.isOpen("table") ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
{collapsed["table"] ? (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
<span className="text-xs font-medium"> </span>
</div>
{sections.isOpen("table") && <div className="space-y-3 border-t p-3">
{!collapsed["table"] && <div className="space-y-3 border-t p-3">
{/* 읽기 테이블 (display 섹션이 있을 때만) */}
{hasDisplayFields && (
<>
@ -837,19 +839,19 @@ function SaveTabContent({
<div className="rounded-md border bg-card">
<div
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
onClick={() => sections.toggle("read")}
onClick={() => toggleSection("read")}
>
{sections.isOpen("read") ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
{collapsed["read"] ? (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
<span className="text-xs font-medium"> </span>
<span className="text-[10px] text-muted-foreground">
( )
</span>
</div>
{sections.isOpen("read") && <div className="space-y-2 border-t p-3">
{!collapsed["read"] && <div className="space-y-2 border-t p-3">
{readColumns.length === 0 ? (
<p className="py-2 text-xs text-muted-foreground">
...
@ -986,19 +988,19 @@ function SaveTabContent({
<div className="rounded-md border bg-card">
<div
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
onClick={() => sections.toggle("input")}
onClick={() => toggleSection("input")}
>
{sections.isOpen("input") ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
{collapsed["input"] ? (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
<span className="text-xs font-medium"> </span>
<span className="text-[10px] text-muted-foreground">
( )
</span>
</div>
{sections.isOpen("input") && <div className="space-y-2 border-t p-3">
{!collapsed["input"] && <div className="space-y-2 border-t p-3">
{saveColumns.length === 0 ? (
<p className="py-2 text-xs text-muted-foreground">
...
@ -1048,19 +1050,19 @@ function SaveTabContent({
<div className="rounded-md border bg-card">
<div
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
onClick={() => sections.toggle("hidden")}
onClick={() => toggleSection("hidden")}
>
{sections.isOpen("hidden") ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
{collapsed["hidden"] ? (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
<span className="text-xs font-medium"> </span>
<span className="text-[10px] text-muted-foreground">
(UI , )
</span>
</div>
{sections.isOpen("hidden") && <div className="space-y-3 border-t p-3">
{!collapsed["hidden"] && <div className="space-y-3 border-t p-3">
{hiddenMappings.map((m) => {
const isJson = m.valueSource === "json_extract";
const isStatic = m.valueSource === "static";
@ -1213,19 +1215,19 @@ function SaveTabContent({
<div className="rounded-md border bg-card">
<div
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
onClick={() => sections.toggle("autogen")}
onClick={() => toggleSection("autogen")}
>
{sections.isOpen("autogen") ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
{collapsed["autogen"] ? (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)}
<span className="text-xs font-medium"> </span>
<span className="text-[10px] text-muted-foreground">
( )
</span>
</div>
{sections.isOpen("autogen") && <div className="space-y-3 border-t p-3">
{!collapsed["autogen"] && <div className="space-y-3 border-t p-3">
{autoGenMappings.map((m) => {
const isLinked = !!m.linkedFieldId;
return (
@ -1394,7 +1396,7 @@ function SectionEditor({
onMoveUp,
allSections,
}: SectionEditorProps) {
const [collapsed, setCollapsed] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const resolvedStyle = migrateStyle(section.style);
const sectionFields = section.fields || [];