a8ded6455d
11 패널 일괄 Inv* prefix 통일:
- 통합 (lib/registry/components/X/): button / container / divider / search /
stats / table / title / input → Inv*ConfigPanel
- frontend/components/v2/config-panels/V2FieldConfigPanel → InvFieldConfigPanel
- 옛 v2-* hidden 호환 → InvLegacy{Divider,Text,Button}ConfigPanel
input 통합 컴포넌트 cp 톤 신규 작성 (InvInputConfigPanel):
- 277줄 옛 디자인 → CPVisualGrid 10칸 type 카드 + 타입별 옵션 + FeatureChipGrid
getComponentConfigPanel.tsx 버그 수정 (Codex 검토):
- "stats" key 중복 제거 (옛 StatsCardConfigPanel 이 통합 stats 덮던 silent bug)
- ALIAS 에서 v2-button-primary/v2-divider-line/v2-text-display 제외
(옵션 B 일관성 — 옛 hidden 컴포넌트는 InvLegacy 패널 사용)
- MAP 의 해당 키를 InvLegacy* 로 직접 매핑
호출처 일괄 갱신:
- 각 통합 컴포넌트의 index.ts 7개 (import / config_panel / re-export)
- v2-input/v2-select/v2-divider-line/v2-text-display/v2-button-primary
의 index.ts (config_panel 매핑)
- V2PropertiesPanel.tsx 의 require pattern (v2-input/v2-select)
검증: tsc 우리 영역 0건 / V2FieldConfigPanel 잔재 0건 / 기존 path 잔재 0건
다음 세션: useDbTables hook 추출 + 잔여 V2* cp 마이그 + dead code 정리
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
383 lines
13 KiB
TypeScript
383 lines
13 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* InvContainerConfigPanel — "영역" (container) cp 톤 설정 패널
|
|
*
|
|
* containerType 5종 분기:
|
|
* - section (카드 / 페이퍼 / 평범)
|
|
* - tabs (탭 list 편집)
|
|
* - accordion (다중 펼침 토글)
|
|
* - repeater (최소/최대 행 + 추가 버튼 텍스트)
|
|
* - conditional (필드 + 연산자 + 값)
|
|
*
|
|
* 흐름:
|
|
* ① 컨테이너 종류 (CPVisualGrid 5칸 — type 시각 선택)
|
|
* ② 유형별 설정 (조건부 본체 — type 별 분기)
|
|
* ▾ 공통 설정 (제목 + 패딩 + 투명 등)
|
|
*
|
|
* Reference: notes/gbpark/2026-04-28-cp-panel-standard.md
|
|
*/
|
|
|
|
import React from "react";
|
|
import {
|
|
Square,
|
|
PanelTopOpen,
|
|
ChevronsDownUp,
|
|
Repeat as RepeatIcon,
|
|
GitBranch,
|
|
Plus,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import {
|
|
CPSection,
|
|
CPRow,
|
|
CPGroup,
|
|
CPText,
|
|
CPSelect,
|
|
CPSwitch,
|
|
CPSegment,
|
|
CPNumber,
|
|
CPIconBtn,
|
|
CPVisualGrid,
|
|
FeatureChipGrid,
|
|
Hint,
|
|
SectionLabel,
|
|
} from "@/components/v2/config-panels/_shared/cp";
|
|
import type { ContainerConfig, ContainerTab } from "./types";
|
|
|
|
export interface InvContainerConfigPanelProps {
|
|
config?: ContainerConfig;
|
|
onChange?: (config: ContainerConfig) => void;
|
|
selectedComponent?: { id: string; config?: ContainerConfig; [k: string]: any };
|
|
}
|
|
|
|
export const InvContainerConfigPanel: React.FC<InvContainerConfigPanelProps> = ({
|
|
config,
|
|
onChange,
|
|
selectedComponent,
|
|
}) => {
|
|
const current: ContainerConfig =
|
|
(config as ContainerConfig) || (selectedComponent?.config as ContainerConfig) || {};
|
|
|
|
const patch = (p: Partial<ContainerConfig>) => onChange?.({ ...current, ...p });
|
|
|
|
const containerType = current.containerType || "section";
|
|
const tabs: ContainerTab[] = current.tabs ?? [];
|
|
|
|
return (
|
|
<div style={{ fontFamily: "var(--v5-font-sans)", color: "var(--cp-text)", padding: "0 12px" }}>
|
|
{/* ── ① 컨테이너 종류 ─────────────────────────── */}
|
|
<CPSection title="① 컨테이너 종류" desc="영역의 형태를 골라요">
|
|
<CPVisualGrid
|
|
cols={5}
|
|
cardHeight={62}
|
|
value={containerType}
|
|
onChange={(v) => patch({ containerType: v as ContainerConfig["containerType"] })}
|
|
options={[
|
|
{
|
|
value: "section",
|
|
label: "섹션",
|
|
preview: <Square size={18} />,
|
|
desc: "카드 / 페이퍼 / 평범 박스",
|
|
},
|
|
{
|
|
value: "tabs",
|
|
label: "탭",
|
|
preview: <PanelTopOpen size={18} />,
|
|
desc: "여러 탭으로 콘텐츠 전환",
|
|
},
|
|
{
|
|
value: "accordion",
|
|
label: "아코디언",
|
|
preview: <ChevronsDownUp size={18} />,
|
|
desc: "펼침/접힘 토글",
|
|
},
|
|
{
|
|
value: "repeater",
|
|
label: "반복",
|
|
preview: <RepeatIcon size={18} />,
|
|
desc: "행 단위 반복 입력",
|
|
},
|
|
{
|
|
value: "conditional",
|
|
label: "조건부",
|
|
preview: <GitBranch size={18} />,
|
|
desc: "조건에 따라 표시/숨김",
|
|
},
|
|
]}
|
|
/>
|
|
</CPSection>
|
|
|
|
{/* ── ② 유형별 설정 ─────────────────────────── */}
|
|
<CPSection title="② 유형별 설정" desc="선택한 종류의 옵션">
|
|
{containerType === "section" && (
|
|
<>
|
|
<CPRow label="섹션 스타일">
|
|
<CPSegment
|
|
value={current.sectionVariant || "card"}
|
|
onChange={(v) =>
|
|
patch({ sectionVariant: v as ContainerConfig["sectionVariant"] })
|
|
}
|
|
options={[
|
|
{ value: "card", label: "카드" },
|
|
{ value: "paper", label: "페이퍼" },
|
|
{ value: "plain", label: "평범" },
|
|
]}
|
|
/>
|
|
</CPRow>
|
|
<FeatureChipGrid
|
|
items={[
|
|
{
|
|
key: "collapsible",
|
|
label: "접기 가능",
|
|
desc: "섹션 헤더 클릭 시 본문을 접고 펼 수 있어요.\n긴 콘텐츠를 숨겨 화면 밀도를 높일 때 유용합니다.",
|
|
},
|
|
{
|
|
key: "defaultCollapsed",
|
|
label: "기본 접힘",
|
|
desc: "처음 화면 진입 시 접힌 상태로 표시됩니다.\n[접기 가능] 이 켜져있을 때만 의미가 있어요.",
|
|
},
|
|
]}
|
|
source={current as any}
|
|
onToggle={(k, v) => patch({ [k]: v } as Partial<ContainerConfig>)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{containerType === "tabs" && (
|
|
<>
|
|
<SectionLabel text={`탭 ${tabs.length}개 · 라벨 편집`} />
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
{tabs.map((tab, idx) => (
|
|
<div
|
|
key={tab.id || idx}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 5,
|
|
padding: "4px 6px",
|
|
background: "var(--cp-bg-subtle)",
|
|
border: "1px solid var(--cp-border-subtle)",
|
|
borderRadius: 4,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 9.5,
|
|
color: "var(--cp-text-muted)",
|
|
fontFamily: "var(--v5-font-mono)",
|
|
minWidth: 22,
|
|
}}
|
|
>
|
|
#{idx + 1}
|
|
</span>
|
|
<input
|
|
type="text"
|
|
value={tab.label}
|
|
onChange={(e) => {
|
|
const next = tabs.map((t, i) =>
|
|
i === idx ? { ...t, label: e.target.value } : t,
|
|
);
|
|
patch({ tabs: next });
|
|
}}
|
|
style={{
|
|
flex: 1,
|
|
height: 22,
|
|
padding: "0 6px",
|
|
fontSize: 11.5,
|
|
background: "var(--cp-surface)",
|
|
border: "1px solid var(--cp-border)",
|
|
borderRadius: 3,
|
|
color: "var(--cp-text)",
|
|
outline: "none",
|
|
fontFamily: "var(--v5-font-sans)",
|
|
}}
|
|
placeholder="탭 라벨"
|
|
/>
|
|
<CPIconBtn
|
|
tone="danger"
|
|
size={20}
|
|
onClick={() => patch({ tabs: tabs.filter((_, i) => i !== idx) })}
|
|
>
|
|
<Trash2 size={10} />
|
|
</CPIconBtn>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
patch({
|
|
tabs: [
|
|
...tabs,
|
|
{ id: `tab${tabs.length + 1}`, label: `탭 ${tabs.length + 1}` },
|
|
],
|
|
})
|
|
}
|
|
style={{
|
|
marginTop: 6,
|
|
width: "100%",
|
|
padding: "5px 8px",
|
|
fontSize: 10.5,
|
|
background: "var(--cp-bg-subtle)",
|
|
border: "1px dashed var(--cp-border)",
|
|
borderRadius: 4,
|
|
cursor: "pointer",
|
|
color: "var(--cp-text-sec)",
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 4,
|
|
fontFamily: "var(--v5-font-sans)",
|
|
}}
|
|
>
|
|
<Plus size={11} /> 탭 추가
|
|
</button>
|
|
{tabs.length > 0 && (
|
|
<CPRow label="기본 선택 탭" help="화면 진입 시 활성화될 탭 ID">
|
|
<CPSelect
|
|
value={current.defaultTab || ""}
|
|
onChange={(v) => patch({ defaultTab: v || undefined })}
|
|
searchable={false}
|
|
>
|
|
<option value="">자동 (첫 탭)</option>
|
|
{tabs.map((t) => (
|
|
<option key={t.id} value={t.id}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</CPSelect>
|
|
</CPRow>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{containerType === "accordion" && (
|
|
<FeatureChipGrid
|
|
items={[
|
|
{
|
|
key: "multiple",
|
|
label: "다중 펼침",
|
|
desc: "여러 항목을 동시에 펼칠 수 있어요.\nOFF 일 때는 하나 펼치면 다른 항목이 자동으로 닫힙니다 (라디오 방식).",
|
|
},
|
|
{
|
|
key: "defaultCollapsed",
|
|
label: "기본 접힘",
|
|
desc: "처음 진입 시 모든 항목이 접힌 상태로 표시됩니다.",
|
|
},
|
|
]}
|
|
source={current as any}
|
|
onToggle={(k, v) => patch({ [k]: v } as Partial<ContainerConfig>)}
|
|
/>
|
|
)}
|
|
|
|
{containerType === "repeater" && (
|
|
<>
|
|
<CPRow label="추가 버튼 텍스트" help="비어있으면 '+ 행 추가' 기본">
|
|
<CPText
|
|
value={current.addRowText || ""}
|
|
onChange={(v) => patch({ addRowText: v || undefined })}
|
|
placeholder="+ 행 추가"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="최소 행 수">
|
|
<CPNumber
|
|
value={current.minRows ?? undefined}
|
|
onChange={(v) => patch({ minRows: v })}
|
|
min={0}
|
|
placeholder="제한 없음"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="최대 행 수">
|
|
<CPNumber
|
|
value={current.maxRows ?? undefined}
|
|
onChange={(v) => patch({ maxRows: v })}
|
|
min={0}
|
|
placeholder="제한 없음"
|
|
/>
|
|
</CPRow>
|
|
<Hint>
|
|
repeater 컨테이너는 단순 반복용. 데이터 검색/매핑이 필요하면 좌측 팔레트의
|
|
[데이터 조회/선택] 컴포넌트를 사용하세요.
|
|
</Hint>
|
|
</>
|
|
)}
|
|
|
|
{containerType === "conditional" && (
|
|
<>
|
|
<CPRow label="조건 필드" help="값을 비교할 필드명 (예: status)">
|
|
<CPText
|
|
value={current.conditionField || ""}
|
|
onChange={(v) => patch({ conditionField: v || undefined })}
|
|
placeholder="status"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="연산자">
|
|
<CPSegment
|
|
value={current.conditionOperator || "="}
|
|
onChange={(v) =>
|
|
patch({ conditionOperator: v as ContainerConfig["conditionOperator"] })
|
|
}
|
|
options={[
|
|
{ value: "=", label: "=" },
|
|
{ value: "!=", label: "≠" },
|
|
{ value: ">", label: ">" },
|
|
{ value: "<", label: "<" },
|
|
{ value: "contains", label: "포함" },
|
|
]}
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="비교 값">
|
|
<CPText
|
|
value={current.conditionValue || ""}
|
|
onChange={(v) => patch({ conditionValue: v || undefined })}
|
|
placeholder="ACTIVE"
|
|
/>
|
|
</CPRow>
|
|
<Hint>
|
|
{current.conditionField || "?"} {current.conditionOperator || "="}{" "}
|
|
<span style={{ color: "var(--v5-primary, #6c5ce7)", fontWeight: 600 }}>
|
|
{current.conditionValue || "?"}
|
|
</span>{" "}
|
|
일 때만 영역이 표시됩니다.
|
|
</Hint>
|
|
</>
|
|
)}
|
|
</CPSection>
|
|
|
|
{/* ── ▾ 공통 설정 ─────────────────────────── */}
|
|
<CPGroup title="공통 설정" defaultOpen={false}>
|
|
<CPRow label="제목" help="섹션/탭 헤더에 표시 (선택)">
|
|
<CPText
|
|
value={current.title || ""}
|
|
onChange={(v) => patch({ title: v || undefined })}
|
|
placeholder="컨테이너 제목"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="내부 패딩" help="CSS — 예: 12px / 8px 16px">
|
|
<CPText
|
|
value={current.padding || ""}
|
|
onChange={(v) => patch({ padding: v || undefined })}
|
|
placeholder="12px"
|
|
/>
|
|
</CPRow>
|
|
<FeatureChipGrid
|
|
items={[
|
|
{
|
|
key: "transparent",
|
|
label: "배경 투명",
|
|
desc: "컨테이너 배경을 투명하게 처리합니다.\n부모 영역의 배경/그라데이션을 그대로 보여주고 싶을 때 사용해요.",
|
|
},
|
|
]}
|
|
source={current as any}
|
|
onToggle={(k, v) => patch({ [k]: v } as Partial<ContainerConfig>)}
|
|
/>
|
|
</CPGroup>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
InvContainerConfigPanel.displayName = "InvContainerConfigPanel";
|
|
|
|
export default InvContainerConfigPanel;
|