feat(pop): 설정 패널 아코디언 접기/펼치기 일관성 + sessionStorage 상태 기억
설정 패널을 열 때 섹션이 일부는 펼쳐져 있고 일부는 접혀 있어 일관성이 없던 UX를 개선하고, 사용자가 펼친 섹션을 탭 세션 내에서 기억한다. - useCollapsibleSections 커스텀 훅 생성 (sessionStorage 기반, 초기 모두 접힘) - PopCardListConfig: CollapsibleSection에 sectionKey/sections prop 패턴 적용 - PopFieldConfig: SaveTabContent 5개 고정 섹션 훅 적용, SectionEditor 초기값 접힘으로 변경 - PopDashboardConfig: PageEditor 초기값 접힘으로 변경
This commit is contained in:
parent
7a9a705f19
commit
12a8290873
|
|
@ -26,5 +26,8 @@ export { useConnectionResolver } from "./useConnectionResolver";
|
||||||
export { useCartSync } from "./useCartSync";
|
export { useCartSync } from "./useCartSync";
|
||||||
export type { UseCartSyncReturn } from "./useCartSync";
|
export type { UseCartSyncReturn } from "./useCartSync";
|
||||||
|
|
||||||
|
// 설정 패널 접기/펼치기 상태 관리
|
||||||
|
export { useCollapsibleSections } from "./useCollapsibleSections";
|
||||||
|
|
||||||
// SQL 빌더 유틸 (고급 사용 시)
|
// SQL 빌더 유틸 (고급 사용 시)
|
||||||
export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder";
|
export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder";
|
||||||
|
|
|
||||||
|
|
@ -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<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 };
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { ChevronDown, ChevronRight, Plus, Trash2, Database, Check } from "lucide-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 type { GridMode } from "@/components/pop/designer/types/pop-layout";
|
||||||
import { GRID_BREAKPOINTS } from "@/components/pop/designer/types/pop-layout";
|
import { GRID_BREAKPOINTS } from "@/components/pop/designer/types/pop-layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -135,6 +136,7 @@ const COLOR_OPTIONS = [
|
||||||
|
|
||||||
export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentColSpan }: ConfigPanelProps) {
|
export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentColSpan }: ConfigPanelProps) {
|
||||||
const [activeTab, setActiveTab] = useState<"basic" | "template">("basic");
|
const [activeTab, setActiveTab] = useState<"basic" | "template">("basic");
|
||||||
|
const sections = useCollapsibleSections("pop-card-list");
|
||||||
|
|
||||||
const cfg: PopCardListConfig = config || DEFAULT_CONFIG;
|
const cfg: PopCardListConfig = config || DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
|
@ -184,6 +186,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
onUpdate={updateConfig}
|
onUpdate={updateConfig}
|
||||||
currentMode={currentMode}
|
currentMode={currentMode}
|
||||||
currentColSpan={currentColSpan}
|
currentColSpan={currentColSpan}
|
||||||
|
sections={sections}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === "template" && (
|
{activeTab === "template" && (
|
||||||
|
|
@ -195,7 +198,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CardTemplateTab config={cfg} onUpdate={updateConfig} />
|
<CardTemplateTab config={cfg} onUpdate={updateConfig} sections={sections} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,16 +208,20 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
|
|
||||||
// ===== 기본 설정 탭 (테이블 + 레이아웃 통합) =====
|
// ===== 기본 설정 탭 (테이블 + 레이아웃 통합) =====
|
||||||
|
|
||||||
|
type SectionsApi = { isOpen: (key: string) => boolean; toggle: (key: string) => void };
|
||||||
|
|
||||||
function BasicSettingsTab({
|
function BasicSettingsTab({
|
||||||
config,
|
config,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
currentMode,
|
currentMode,
|
||||||
currentColSpan,
|
currentColSpan,
|
||||||
|
sections,
|
||||||
}: {
|
}: {
|
||||||
config: PopCardListConfig;
|
config: PopCardListConfig;
|
||||||
onUpdate: (partial: Partial<PopCardListConfig>) => void;
|
onUpdate: (partial: Partial<PopCardListConfig>) => void;
|
||||||
currentMode?: GridMode;
|
currentMode?: GridMode;
|
||||||
currentColSpan?: number;
|
currentColSpan?: number;
|
||||||
|
sections: SectionsApi;
|
||||||
}) {
|
}) {
|
||||||
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
|
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
|
||||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
|
|
@ -321,7 +328,7 @@ function BasicSettingsTab({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 장바구니 목록 모드 */}
|
{/* 장바구니 목록 모드 */}
|
||||||
<CollapsibleSection title="장바구니 목록 모드" defaultOpen={isCartListMode}>
|
<CollapsibleSection sectionKey="basic-cart-mode" title="장바구니 목록 모드" sections={sections}>
|
||||||
<CartListModeSection
|
<CartListModeSection
|
||||||
cartListMode={config.cartListMode}
|
cartListMode={config.cartListMode}
|
||||||
onUpdate={(cartListMode) => onUpdate({ cartListMode })}
|
onUpdate={(cartListMode) => onUpdate({ cartListMode })}
|
||||||
|
|
@ -330,7 +337,7 @@ function BasicSettingsTab({
|
||||||
|
|
||||||
{/* 테이블 선택 (장바구니 모드 시 숨김) */}
|
{/* 테이블 선택 (장바구니 모드 시 숨김) */}
|
||||||
{!isCartListMode && (
|
{!isCartListMode && (
|
||||||
<CollapsibleSection title="테이블 선택" defaultOpen>
|
<CollapsibleSection sectionKey="basic-table" title="테이블 선택" sections={sections}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-[10px] text-muted-foreground">데이터 테이블</Label>
|
<Label className="text-[10px] text-muted-foreground">데이터 테이블</Label>
|
||||||
|
|
@ -365,7 +372,9 @@ function BasicSettingsTab({
|
||||||
{/* 조인 설정 (장바구니 모드 시 숨김) */}
|
{/* 조인 설정 (장바구니 모드 시 숨김) */}
|
||||||
{!isCartListMode && dataSource.tableName && (
|
{!isCartListMode && dataSource.tableName && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
sectionKey="basic-join"
|
||||||
title="조인 설정"
|
title="조인 설정"
|
||||||
|
sections={sections}
|
||||||
badge={
|
badge={
|
||||||
dataSource.joins && dataSource.joins.length > 0
|
dataSource.joins && dataSource.joins.length > 0
|
||||||
? `${dataSource.joins.length}개`
|
? `${dataSource.joins.length}개`
|
||||||
|
|
@ -383,7 +392,9 @@ function BasicSettingsTab({
|
||||||
{/* 정렬 기준 (장바구니 모드 시 숨김) */}
|
{/* 정렬 기준 (장바구니 모드 시 숨김) */}
|
||||||
{!isCartListMode && dataSource.tableName && (
|
{!isCartListMode && dataSource.tableName && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
sectionKey="basic-sort"
|
||||||
title="정렬 기준"
|
title="정렬 기준"
|
||||||
|
sections={sections}
|
||||||
badge={
|
badge={
|
||||||
dataSource.sort
|
dataSource.sort
|
||||||
? Array.isArray(dataSource.sort)
|
? Array.isArray(dataSource.sort)
|
||||||
|
|
@ -403,7 +414,9 @@ function BasicSettingsTab({
|
||||||
{/* 필터 기준 (장바구니 모드 시 숨김) */}
|
{/* 필터 기준 (장바구니 모드 시 숨김) */}
|
||||||
{!isCartListMode && dataSource.tableName && (
|
{!isCartListMode && dataSource.tableName && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
sectionKey="basic-filter"
|
||||||
title="필터 기준"
|
title="필터 기준"
|
||||||
|
sections={sections}
|
||||||
badge={
|
badge={
|
||||||
dataSource.filters && dataSource.filters.length > 0
|
dataSource.filters && dataSource.filters.length > 0
|
||||||
? `${dataSource.filters.length}개`
|
? `${dataSource.filters.length}개`
|
||||||
|
|
@ -421,7 +434,9 @@ function BasicSettingsTab({
|
||||||
{/* 저장 매핑 (장바구니 모드일 때만) */}
|
{/* 저장 매핑 (장바구니 모드일 때만) */}
|
||||||
{isCartListMode && (
|
{isCartListMode && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
sectionKey="basic-save-mapping"
|
||||||
title="저장 매핑"
|
title="저장 매핑"
|
||||||
|
sections={sections}
|
||||||
badge={
|
badge={
|
||||||
config.saveMapping?.mappings && config.saveMapping.mappings.length > 0
|
config.saveMapping?.mappings && config.saveMapping.mappings.length > 0
|
||||||
? `${config.saveMapping.mappings.length}개`
|
? `${config.saveMapping.mappings.length}개`
|
||||||
|
|
@ -437,7 +452,7 @@ function BasicSettingsTab({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 레이아웃 설정 */}
|
{/* 레이아웃 설정 */}
|
||||||
<CollapsibleSection title="레이아웃 설정" defaultOpen>
|
<CollapsibleSection sectionKey="basic-layout" title="레이아웃 설정" sections={sections}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{modeLabel && (
|
{modeLabel && (
|
||||||
<div className="flex items-center gap-1.5 rounded-md bg-muted/50 px-2.5 py-1.5">
|
<div className="flex items-center gap-1.5 rounded-md bg-muted/50 px-2.5 py-1.5">
|
||||||
|
|
@ -526,9 +541,11 @@ function BasicSettingsTab({
|
||||||
function CardTemplateTab({
|
function CardTemplateTab({
|
||||||
config,
|
config,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
sections,
|
||||||
}: {
|
}: {
|
||||||
config: PopCardListConfig;
|
config: PopCardListConfig;
|
||||||
onUpdate: (partial: Partial<PopCardListConfig>) => void;
|
onUpdate: (partial: Partial<PopCardListConfig>) => void;
|
||||||
|
sections: SectionsApi;
|
||||||
}) {
|
}) {
|
||||||
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
|
const dataSource = config.dataSource || DEFAULT_DATA_SOURCE;
|
||||||
const template = config.cardTemplate || DEFAULT_TEMPLATE;
|
const template = config.cardTemplate || DEFAULT_TEMPLATE;
|
||||||
|
|
@ -634,7 +651,7 @@ function CardTemplateTab({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 헤더 설정 */}
|
{/* 헤더 설정 */}
|
||||||
<CollapsibleSection title="헤더 설정" defaultOpen>
|
<CollapsibleSection sectionKey="tpl-header" title="헤더 설정" sections={sections}>
|
||||||
<HeaderSettingsSection
|
<HeaderSettingsSection
|
||||||
header={template.header || DEFAULT_HEADER}
|
header={template.header || DEFAULT_HEADER}
|
||||||
columnGroups={columnGroups}
|
columnGroups={columnGroups}
|
||||||
|
|
@ -643,7 +660,7 @@ function CardTemplateTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 이미지 설정 */}
|
{/* 이미지 설정 */}
|
||||||
<CollapsibleSection title="이미지 설정" defaultOpen>
|
<CollapsibleSection sectionKey="tpl-image" title="이미지 설정" sections={sections}>
|
||||||
<ImageSettingsSection
|
<ImageSettingsSection
|
||||||
image={template.image || DEFAULT_IMAGE}
|
image={template.image || DEFAULT_IMAGE}
|
||||||
columnGroups={columnGroups}
|
columnGroups={columnGroups}
|
||||||
|
|
@ -653,9 +670,10 @@ function CardTemplateTab({
|
||||||
|
|
||||||
{/* 본문 필드 */}
|
{/* 본문 필드 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
sectionKey="tpl-body"
|
||||||
title="본문 필드"
|
title="본문 필드"
|
||||||
|
sections={sections}
|
||||||
badge={`${template.body?.fields?.length || 0}개`}
|
badge={`${template.body?.fields?.length || 0}개`}
|
||||||
defaultOpen
|
|
||||||
>
|
>
|
||||||
<BodyFieldsSection
|
<BodyFieldsSection
|
||||||
body={template.body || DEFAULT_BODY}
|
body={template.body || DEFAULT_BODY}
|
||||||
|
|
@ -665,7 +683,7 @@ function CardTemplateTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 입력 필드 설정 */}
|
{/* 입력 필드 설정 */}
|
||||||
<CollapsibleSection title="입력 필드" defaultOpen={false}>
|
<CollapsibleSection sectionKey="tpl-input" title="입력 필드" sections={sections}>
|
||||||
<InputFieldSettingsSection
|
<InputFieldSettingsSection
|
||||||
inputField={config.inputField}
|
inputField={config.inputField}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
@ -675,7 +693,7 @@ function CardTemplateTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 포장등록 설정 */}
|
{/* 포장등록 설정 */}
|
||||||
<CollapsibleSection title="포장등록 (계산기)" defaultOpen={false}>
|
<CollapsibleSection sectionKey="tpl-package" title="포장등록 (계산기)" sections={sections}>
|
||||||
<PackageSettingsSection
|
<PackageSettingsSection
|
||||||
packageConfig={config.packageConfig}
|
packageConfig={config.packageConfig}
|
||||||
onUpdate={(packageConfig) => onUpdate({ packageConfig })}
|
onUpdate={(packageConfig) => onUpdate({ packageConfig })}
|
||||||
|
|
@ -683,7 +701,7 @@ function CardTemplateTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 담기 버튼 설정 */}
|
{/* 담기 버튼 설정 */}
|
||||||
<CollapsibleSection title="담기 버튼" defaultOpen={false}>
|
<CollapsibleSection sectionKey="tpl-cart" title="담기 버튼" sections={sections}>
|
||||||
<CartActionSettingsSection
|
<CartActionSettingsSection
|
||||||
cartAction={config.cartAction}
|
cartAction={config.cartAction}
|
||||||
onUpdate={(cartAction) => onUpdate({ cartAction })}
|
onUpdate={(cartAction) => onUpdate({ cartAction })}
|
||||||
|
|
@ -693,7 +711,7 @@ function CardTemplateTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 반응형 표시 설정 */}
|
{/* 반응형 표시 설정 */}
|
||||||
<CollapsibleSection title="반응형 표시" defaultOpen={false}>
|
<CollapsibleSection sectionKey="tpl-responsive" title="반응형 표시" sections={sections}>
|
||||||
<ResponsiveDisplaySection
|
<ResponsiveDisplaySection
|
||||||
config={config}
|
config={config}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
|
|
@ -769,24 +787,26 @@ function GroupedColumnSelect({
|
||||||
// ===== 접기/펴기 섹션 컴포넌트 =====
|
// ===== 접기/펴기 섹션 컴포넌트 =====
|
||||||
|
|
||||||
function CollapsibleSection({
|
function CollapsibleSection({
|
||||||
|
sectionKey,
|
||||||
title,
|
title,
|
||||||
badge,
|
badge,
|
||||||
defaultOpen = false,
|
sections,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
sectionKey: string;
|
||||||
title: string;
|
title: string;
|
||||||
badge?: string;
|
badge?: string;
|
||||||
defaultOpen?: boolean;
|
sections: SectionsApi;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
const open = sections.isOpen(sectionKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50"
|
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50"
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => sections.toggle(sectionKey)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{open ? (
|
{open ? (
|
||||||
|
|
|
||||||
|
|
@ -1937,7 +1937,7 @@ function PageEditor({
|
||||||
isPreviewing?: boolean;
|
isPreviewing?: boolean;
|
||||||
onUpdateItem?: (updatedItem: DashboardItem) => void;
|
onUpdateItem?: (updatedItem: DashboardItem) => void;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border p-2">
|
<div className="rounded-md border p-2">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
|
import { useCollapsibleSections } from "@/hooks/pop/useCollapsibleSections";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
|
@ -648,10 +649,7 @@ function SaveTabContent({
|
||||||
|
|
||||||
const noFields = allFields.length === 0;
|
const noFields = allFields.length === 0;
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
const sections = useCollapsibleSections("pop-field");
|
||||||
const toggleSection = useCallback((key: string) => {
|
|
||||||
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -666,16 +664,16 @@ function SaveTabContent({
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
||||||
onClick={() => toggleSection("table")}
|
onClick={() => sections.toggle("table")}
|
||||||
>
|
>
|
||||||
{collapsed["table"] ? (
|
{sections.isOpen("table") ? (
|
||||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium">테이블 설정</span>
|
<span className="text-xs font-medium">테이블 설정</span>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed["table"] && <div className="space-y-3 border-t p-3">
|
{sections.isOpen("table") && <div className="space-y-3 border-t p-3">
|
||||||
{/* 읽기 테이블 (display 섹션이 있을 때만) */}
|
{/* 읽기 테이블 (display 섹션이 있을 때만) */}
|
||||||
{hasDisplayFields && (
|
{hasDisplayFields && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -839,19 +837,19 @@ function SaveTabContent({
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
||||||
onClick={() => toggleSection("read")}
|
onClick={() => sections.toggle("read")}
|
||||||
>
|
>
|
||||||
{collapsed["read"] ? (
|
{sections.isOpen("read") ? (
|
||||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium">읽기 필드</span>
|
<span className="text-xs font-medium">읽기 필드</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
(읽기 폼)
|
(읽기 폼)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed["read"] && <div className="space-y-2 border-t p-3">
|
{sections.isOpen("read") && <div className="space-y-2 border-t p-3">
|
||||||
{readColumns.length === 0 ? (
|
{readColumns.length === 0 ? (
|
||||||
<p className="py-2 text-xs text-muted-foreground">
|
<p className="py-2 text-xs text-muted-foreground">
|
||||||
읽기 테이블의 컬럼을 불러오는 중...
|
읽기 테이블의 컬럼을 불러오는 중...
|
||||||
|
|
@ -988,19 +986,19 @@ function SaveTabContent({
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
||||||
onClick={() => toggleSection("input")}
|
onClick={() => sections.toggle("input")}
|
||||||
>
|
>
|
||||||
{collapsed["input"] ? (
|
{sections.isOpen("input") ? (
|
||||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium">입력 필드</span>
|
<span className="text-xs font-medium">입력 필드</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
(입력 폼 → 저장)
|
(입력 폼 → 저장)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed["input"] && <div className="space-y-2 border-t p-3">
|
{sections.isOpen("input") && <div className="space-y-2 border-t p-3">
|
||||||
{saveColumns.length === 0 ? (
|
{saveColumns.length === 0 ? (
|
||||||
<p className="py-2 text-xs text-muted-foreground">
|
<p className="py-2 text-xs text-muted-foreground">
|
||||||
저장 테이블의 컬럼을 불러오는 중...
|
저장 테이블의 컬럼을 불러오는 중...
|
||||||
|
|
@ -1050,19 +1048,19 @@ function SaveTabContent({
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
||||||
onClick={() => toggleSection("hidden")}
|
onClick={() => sections.toggle("hidden")}
|
||||||
>
|
>
|
||||||
{collapsed["hidden"] ? (
|
{sections.isOpen("hidden") ? (
|
||||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium">숨은 필드</span>
|
<span className="text-xs font-medium">숨은 필드</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
(UI 미표시, 전달 데이터에서 추출하여 저장)
|
(UI 미표시, 전달 데이터에서 추출하여 저장)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed["hidden"] && <div className="space-y-3 border-t p-3">
|
{sections.isOpen("hidden") && <div className="space-y-3 border-t p-3">
|
||||||
{hiddenMappings.map((m) => {
|
{hiddenMappings.map((m) => {
|
||||||
const isJson = m.valueSource === "json_extract";
|
const isJson = m.valueSource === "json_extract";
|
||||||
const isStatic = m.valueSource === "static";
|
const isStatic = m.valueSource === "static";
|
||||||
|
|
@ -1215,19 +1213,19 @@ function SaveTabContent({
|
||||||
<div className="rounded-md border bg-card">
|
<div className="rounded-md border bg-card">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
className="flex cursor-pointer items-center gap-1.5 px-3 py-2"
|
||||||
onClick={() => toggleSection("autogen")}
|
onClick={() => sections.toggle("autogen")}
|
||||||
>
|
>
|
||||||
{collapsed["autogen"] ? (
|
{sections.isOpen("autogen") ? (
|
||||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium">자동생성 필드</span>
|
<span className="text-xs font-medium">자동생성 필드</span>
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
(저장 시 서버에서 채번)
|
(저장 시 서버에서 채번)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed["autogen"] && <div className="space-y-3 border-t p-3">
|
{sections.isOpen("autogen") && <div className="space-y-3 border-t p-3">
|
||||||
{autoGenMappings.map((m) => {
|
{autoGenMappings.map((m) => {
|
||||||
const isLinked = !!m.linkedFieldId;
|
const isLinked = !!m.linkedFieldId;
|
||||||
return (
|
return (
|
||||||
|
|
@ -1396,7 +1394,7 @@ function SectionEditor({
|
||||||
onMoveUp,
|
onMoveUp,
|
||||||
allSections,
|
allSections,
|
||||||
}: SectionEditorProps) {
|
}: SectionEditorProps) {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const resolvedStyle = migrateStyle(section.style);
|
const resolvedStyle = migrateStyle(section.style);
|
||||||
|
|
||||||
const sectionFields = section.fields || [];
|
const sectionFields = section.fields || [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue