refactor: ConfigPanel Inv 네이밍 통합 + legacy 패널 분리 + input cp 마이그

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>
This commit is contained in:
DDD1542
2026-04-28 17:57:57 +09:00
parent 8b8186d1c0
commit a8ded6455d
28 changed files with 491 additions and 385 deletions
@@ -213,8 +213,8 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
// 🆕 V2 컴포넌트 직접 감지 및 설정 패널 렌더링
if (componentId?.startsWith("v2-")) {
const v2ConfigPanels: Record<string, React.FC<{ config: any; onChange: (config: any) => void }>> = {
"v2-input": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel,
"v2-select": require("@/components/v2/config-panels/V2FieldConfigPanel").V2FieldConfigPanel,
"v2-input": require("@/components/v2/config-panels/InvFieldConfigPanel").InvFieldConfigPanel,
"v2-select": require("@/components/v2/config-panels/InvFieldConfigPanel").InvFieldConfigPanel,
"v2-date": require("@/components/v2/config-panels/V2DateConfigPanel").V2DateConfigPanel,
// v2-list / v2-repeater / v2-table-list 는 InvDataConfigPanel 통합 — 하드코딩 매핑 제거 → ComponentRegistry fallback 사용
"v2-layout": require("@/components/v2/config-panels/V2LayoutConfigPanel").V2LayoutConfigPanel,
@@ -44,6 +44,7 @@ import {
CPSwitch,
CPNumber,
CPSegment,
CPColor,
CPIconBtn,
CPCrumb,
CPFormatTrigger,
@@ -407,7 +408,7 @@ function applyTriple(
}
// ─────────────────────────────────────────────────────────
interface V2FieldConfigPanelProps {
interface InvFieldConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
tableName?: string;
@@ -419,7 +420,7 @@ interface V2FieldConfigPanelProps {
componentType?: string;
}
export const V2FieldConfigPanel: React.FC<V2FieldConfigPanelProps> = ({
export const InvFieldConfigPanel: React.FC<InvFieldConfigPanelProps> = ({
config,
onChange,
tableName,
@@ -786,7 +787,7 @@ export const V2FieldConfigPanel: React.FC<V2FieldConfigPanelProps> = ({
);
};
V2FieldConfigPanel.displayName = "V2FieldConfigPanel";
InvFieldConfigPanel.displayName = "InvFieldConfigPanel";
// ───────────────────────────────────────────────────────
// 유틸 컴포넌트
@@ -2188,4 +2189,4 @@ const FilterConditionsSection: React.FC<{
);
};
export default V2FieldConfigPanel;
export default InvFieldConfigPanel;
@@ -1,7 +1,7 @@
"use client";
/**
* InvButtonConfigPanel (v2-button-primary) cp
* InvLegacyButtonConfigPanel (v2-button-primary) cp
*
* :
* (CPVisualGrid 14 )
@@ -131,7 +131,7 @@ interface ScreenOption {
description?: string;
}
interface InvButtonConfigPanelProps {
interface InvLegacyButtonConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
component?: ComponentData;
@@ -147,7 +147,7 @@ interface InvButtonConfigPanelProps {
// ───────────────────────────────────────────────────────
// Main Component
// ───────────────────────────────────────────────────────
export const InvButtonConfigPanel: React.FC<InvButtonConfigPanelProps> = ({
export const InvLegacyButtonConfigPanel: React.FC<InvLegacyButtonConfigPanelProps> = ({
config,
onChange,
component,
@@ -582,7 +582,7 @@ export const InvButtonConfigPanel: React.FC<InvButtonConfigPanelProps> = ({
);
};
InvButtonConfigPanel.displayName = "InvButtonConfigPanel";
InvLegacyButtonConfigPanel.displayName = "InvLegacyButtonConfigPanel";
// ───────────────────────────────────────────────────────
// describeAction — ④ 섹션 desc 동적 텍스트
@@ -1712,4 +1712,4 @@ function TransferDataMappingBody(p: TransferDataMappingBodyProps) {
);
}
export default InvButtonConfigPanel;
export default InvLegacyButtonConfigPanel;
@@ -1,7 +1,7 @@
"use client";
/**
* InvDividerConfigPanel (v2-divider-line) cp
* InvLegacyDividerConfigPanel (v2-divider-line) cp
*
* :
* + (CPVisualGrid )
@@ -24,7 +24,7 @@ import {
Hint,
} from "./_shared/cp";
interface InvDividerConfigPanelProps {
interface InvLegacyDividerConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
}
@@ -32,7 +32,7 @@ interface InvDividerConfigPanelProps {
const DEFAULT_LINE_COLOR = "#d1d5db";
const DEFAULT_TEXT_COLOR = "#6b7280";
export const InvDividerConfigPanel: React.FC<InvDividerConfigPanelProps> = ({
export const InvLegacyDividerConfigPanel: React.FC<InvLegacyDividerConfigPanelProps> = ({
config,
onChange,
}) => {
@@ -162,7 +162,7 @@ export const InvDividerConfigPanel: React.FC<InvDividerConfigPanelProps> = ({
);
};
InvDividerConfigPanel.displayName = "InvDividerConfigPanel";
InvLegacyDividerConfigPanel.displayName = "InvLegacyDividerConfigPanel";
// 작은 헬퍼 — 색 점 (visual preview)
function ColorDot({ color }: { color: string }) {
@@ -180,4 +180,4 @@ function ColorDot({ color }: { color: string }) {
);
}
export default InvDividerConfigPanel;
export default InvLegacyDividerConfigPanel;
@@ -1,7 +1,7 @@
"use client";
/**
* InvTextConfigPanel / (v2-text-display) cp
* InvLegacyTextConfigPanel / (v2-text-display) cp
*
* :
* CPText
@@ -26,12 +26,12 @@ import {
} from "./_shared/cp";
import { TextDisplayConfig } from "@/lib/registry/components/v2-text-display/types";
interface InvTextConfigPanelProps {
interface InvLegacyTextConfigPanelProps {
config: TextDisplayConfig;
onChange: (config: Partial<TextDisplayConfig>) => void;
}
export const InvTextConfigPanel: React.FC<InvTextConfigPanelProps> = ({ config, onChange }) => {
export const InvLegacyTextConfigPanel: React.FC<InvLegacyTextConfigPanelProps> = ({ config, onChange }) => {
const update = (field: keyof TextDisplayConfig, value: any) => {
const next = { ...config, [field]: value };
onChange({ [field]: value });
@@ -200,6 +200,6 @@ export const InvTextConfigPanel: React.FC<InvTextConfigPanelProps> = ({ config,
);
};
InvTextConfigPanel.displayName = "InvTextConfigPanel";
InvLegacyTextConfigPanel.displayName = "InvLegacyTextConfigPanel";
export default InvTextConfigPanel;
export default InvLegacyTextConfigPanel;
@@ -1,7 +1,7 @@
"use client";
/**
* ButtonConfigPanel "버튼" (id: button) cp
* InvButtonConfigPanel "버튼" (id: button) cp
*
* :
*
@@ -10,7 +10,7 @@
* IconPicker +
* (FeatureChipGrid)
*
* v2-button-primary InvButtonConfigPanel ( ).
* v2-button-primary InvInvButtonConfigPanel ( ).
* "button" .
*
* Reference: notes/gbpark/2026-04-28-cp-panel-standard.md
@@ -46,7 +46,7 @@ import {
import { IconPicker } from "../common/IconPicker";
import type { ButtonConfig } from "./types";
export interface ButtonConfigPanelProps {
export interface InvButtonConfigPanelProps {
config?: ButtonConfig;
onChange?: (config: ButtonConfig) => void;
onUpdateProperty?: (componentId: string, path: string, value: unknown) => void;
@@ -142,7 +142,7 @@ const ACTION_GROUPS: Array<{ id: string; name: string; items: string[] }> = [
// ───────────────────────────────────────────────────────
// Main
// ───────────────────────────────────────────────────────
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
export const InvButtonConfigPanel: React.FC<InvButtonConfigPanelProps> = ({
config,
onChange,
selectedComponent,
@@ -273,7 +273,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
);
};
ButtonConfigPanel.displayName = "ButtonConfigPanel";
InvButtonConfigPanel.displayName = "InvButtonConfigPanel";
// ───────────────────────────────────────────────────────
// ActionCardGrid — 2단계 선택 (그룹 segment + 그 안 chip)
@@ -426,4 +426,4 @@ function ActionChip({
);
}
export default ButtonConfigPanel;
export default InvButtonConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { ButtonWrapper } from "./ButtonComponent";
import { ButtonConfigPanel } from "./ButtonConfigPanel";
import { InvButtonConfigPanel } from "./InvButtonConfigPanel";
import type { ButtonConfig } from "./types";
/**
@@ -42,7 +42,7 @@ export const ButtonDefinition = createComponentDefinition({
component: ButtonWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 120, height: 36 },
config_panel: ButtonConfigPanel,
config_panel: InvButtonConfigPanel,
icon: "MousePointer",
tags: ["버튼", "button", "action", "click"],
version: "2.0.0",
@@ -61,4 +61,4 @@ export const ButtonDefinition = createComponentDefinition({
export type { ButtonConfig } from "./types";
export { ButtonComponent, ButtonWrapper } from "./ButtonComponent";
export { ButtonConfigPanel } from "./ButtonConfigPanel";
export { InvButtonConfigPanel } from "./InvButtonConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* ContainerConfigPanel "영역" (container) cp
* InvContainerConfigPanel "영역" (container) cp
*
* containerType 5 :
* - section ( / / )
@@ -45,13 +45,13 @@ import {
} from "@/components/v2/config-panels/_shared/cp";
import type { ContainerConfig, ContainerTab } from "./types";
export interface ContainerConfigPanelProps {
export interface InvContainerConfigPanelProps {
config?: ContainerConfig;
onChange?: (config: ContainerConfig) => void;
selectedComponent?: { id: string; config?: ContainerConfig; [k: string]: any };
}
export const ContainerConfigPanel: React.FC<ContainerConfigPanelProps> = ({
export const InvContainerConfigPanel: React.FC<InvContainerConfigPanelProps> = ({
config,
onChange,
selectedComponent,
@@ -377,6 +377,6 @@ export const ContainerConfigPanel: React.FC<ContainerConfigPanelProps> = ({
);
};
ContainerConfigPanel.displayName = "ContainerConfigPanel";
InvContainerConfigPanel.displayName = "InvContainerConfigPanel";
export default ContainerConfigPanel;
export default InvContainerConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { ContainerWrapper } from "./ContainerComponent";
import { ContainerConfigPanel } from "./ContainerConfigPanel";
import { InvContainerConfigPanel } from "./InvContainerConfigPanel";
const DEFAULT_CONFIG = {
containerType: "section",
@@ -21,7 +21,7 @@ export const ContainerDefinition = createComponentDefinition({
component: ContainerWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 600, height: 300 },
config_panel: ContainerConfigPanel,
config_panel: InvContainerConfigPanel,
icon: "LayoutGrid",
tags: ["컨테이너", "container", "탭", "섹션", "아코디언", "반복", "layout"],
version: "2.0.0",
@@ -31,4 +31,4 @@ export const ContainerDefinition = createComponentDefinition({
export type { ContainerConfig } from "./types";
export { ContainerComponent, ContainerWrapper } from "./ContainerComponent";
export { ContainerConfigPanel } from "./ContainerConfigPanel";
export { InvContainerConfigPanel } from "./InvContainerConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* DividerConfigPanel "구분선" (id: divider) cp
* InvDividerConfigPanel "구분선" (id: divider) cp
*
* :
* (CPSegment) + (CPVisualGrid ) + (CPVisualGrid )
@@ -26,7 +26,7 @@ import {
} from "@/components/v2/config-panels/_shared/cp";
import type { DividerConfig } from "./types";
export interface DividerConfigPanelProps {
export interface InvDividerConfigPanelProps {
config?: DividerConfig;
onChange?: (config: DividerConfig) => void;
onUpdateProperty?: (componentId: string, path: string, value: unknown) => void;
@@ -35,7 +35,7 @@ export interface DividerConfigPanelProps {
const DEFAULT_COLOR = "#d1d5db";
export const DividerConfigPanel: React.FC<DividerConfigPanelProps> = ({
export const InvDividerConfigPanel: React.FC<InvDividerConfigPanelProps> = ({
config,
onChange,
onUpdateProperty,
@@ -189,6 +189,6 @@ export const DividerConfigPanel: React.FC<DividerConfigPanelProps> = ({
);
};
DividerConfigPanel.displayName = "DividerConfigPanel";
InvDividerConfigPanel.displayName = "InvDividerConfigPanel";
export default DividerConfigPanel;
export default InvDividerConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { DividerWrapper } from "./DividerComponent";
import { DividerConfigPanel } from "./DividerConfigPanel";
import { InvDividerConfigPanel } from "./InvDividerConfigPanel";
import type { DividerConfig } from "./types";
/**
@@ -43,7 +43,7 @@ export const DividerDefinition = createComponentDefinition({
component: DividerWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 400, height: 2 },
config_panel: DividerConfigPanel,
config_panel: InvDividerConfigPanel,
icon: "SeparatorHorizontal",
tags: ["구분선", "divider", "line", "separator", "v2-divider-line"],
version: "2.0.0",
@@ -54,4 +54,4 @@ export const DividerDefinition = createComponentDefinition({
export type { DividerConfig } from "./types";
export { DividerComponent, DividerWrapper } from "./DividerComponent";
export { DividerConfigPanel } from "./DividerConfigPanel";
export { InvDividerConfigPanel } from "./InvDividerConfigPanel";
@@ -1,277 +0,0 @@
"use client";
import React from "react";
import type { InputConfig, InputFieldType } from "./types";
/**
* Input ConfigPanel — 통합 입력 컴포넌트 설정 편집.
*
* 가장 중요한 건 `type` 필드. 선택한 type 에 따라 하위 옵션이 달라진다.
* Phase B-1 의 최소 구현; Phase F 에서 entity ref picker / options builder
* 등 고급 UI 추가 예정.
*/
export interface InputConfigPanelProps {
config?: InputConfig;
onChange?: (config: InputConfig) => void;
selectedComponent?: { id: string; config?: InputConfig; [k: string]: any };
}
export const InputConfigPanel: React.FC<InputConfigPanelProps> = ({
config,
onChange,
selectedComponent,
}) => {
const current: InputConfig =
(config as InputConfig) || (selectedComponent?.config as InputConfig) || {};
const patch = (p: Partial<InputConfig>) => {
onChange?.({ ...current, ...p });
};
const type: InputFieldType = (current.type as InputFieldType) || "text";
return (
<div className="flex flex-col gap-3 p-3 text-xs">
{/* ─── 타입 (최상위) ─── */}
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
</label>
<select
value={type}
onChange={(e) => patch({ type: e.target.value as InputFieldType })}
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
>
<option value="text">text </option>
<option value="number">number </option>
<option value="date">date </option>
<option value="datetime">datetime +</option>
<option value="select">select </option>
<option value="entity">entity FK ( )</option>
<option value="checkbox">checkbox </option>
<option value="textarea">textarea </option>
<option value="file">file </option>
<option value="code">code (readonly)</option>
</select>
</div>
{/* ─── 라벨 / placeholder ─── */}
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
</label>
<input
type="text"
value={current.label || ""}
onChange={(e) => patch({ label: e.target.value })}
placeholder="필드 라벨"
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
placeholder
</label>
<input
type="text"
value={current.placeholder || ""}
onChange={(e) => patch({ placeholder: e.target.value })}
placeholder="입력 안내"
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
</label>
<input
type="text"
value={current.helperText || ""}
onChange={(e) => patch({ helperText: e.target.value || undefined })}
placeholder="하단 도움말"
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
{/* ─── 기본 토글 ─── */}
<label className="flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={!!current.required}
onChange={(e) => patch({ required: e.target.checked })}
/>
<span> </span>
</label>
<label className="flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={current.editable !== false}
onChange={(e) => patch({ editable: e.target.checked })}
/>
<span> </span>
</label>
<label className="flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={!!current.disabled}
onChange={(e) => patch({ disabled: e.target.checked })}
/>
<span></span>
</label>
{/* ─── type 별 하위 옵션 ─── */}
{(type === "number") && (
<>
<div className="border-border mt-2 border-t pt-2">
<div className="text-muted-foreground mb-2 text-[0.6rem] font-semibold tracking-wider uppercase">
number
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<label className="text-muted-foreground mb-1 block text-[0.55rem]">min</label>
<input
type="number"
value={current.min ?? ""}
onChange={(e) => patch({ min: e.target.value === "" ? undefined : Number(e.target.value) })}
className="border-border bg-background w-full rounded border px-1 py-0.5 text-[0.65rem]"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-[0.55rem]">max</label>
<input
type="number"
value={current.max ?? ""}
onChange={(e) => patch({ max: e.target.value === "" ? undefined : Number(e.target.value) })}
className="border-border bg-background w-full rounded border px-1 py-0.5 text-[0.65rem]"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-[0.55rem]">step</label>
<input
type="number"
value={current.step ?? ""}
onChange={(e) => patch({ step: e.target.value === "" ? undefined : Number(e.target.value) })}
className="border-border bg-background w-full rounded border px-1 py-0.5 text-[0.65rem]"
/>
</div>
</div>
</>
)}
{(type === "text" || type === "textarea") && (
<>
<div className="border-border mt-2 border-t pt-2">
<div className="text-muted-foreground mb-2 text-[0.6rem] font-semibold tracking-wider uppercase">
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-muted-foreground mb-1 block text-[0.55rem]">minLength</label>
<input
type="number"
value={current.minLength ?? ""}
onChange={(e) => patch({ minLength: e.target.value === "" ? undefined : Number(e.target.value) })}
className="border-border bg-background w-full rounded border px-1 py-0.5 text-[0.65rem]"
/>
</div>
<div>
<label className="text-muted-foreground mb-1 block text-[0.55rem]">maxLength</label>
<input
type="number"
value={current.maxLength ?? ""}
onChange={(e) => patch({ maxLength: e.target.value === "" ? undefined : Number(e.target.value) })}
className="border-border bg-background w-full rounded border px-1 py-0.5 text-[0.65rem]"
/>
</div>
</div>
</>
)}
{type === "textarea" && (
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
textarea
</label>
<input
type="number"
value={current.rows ?? 3}
onChange={(e) => patch({ rows: Number(e.target.value) })}
min={1}
max={20}
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
)}
{type === "select" && (
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
select ( )
</label>
<input
type="text"
value={Array.isArray(current.options) ? current.options.map((o: any) => (typeof o === "string" ? o : o.label)).join(", ") : ""}
onChange={(e) => {
const options = e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean);
patch({ options });
}}
placeholder="예: 확정, 취소, 대기"
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
)}
{type === "file" && (
<>
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
</label>
<input
type="text"
value={current.accept || ""}
onChange={(e) => patch({ accept: e.target.value || undefined })}
placeholder="예: image/*, .pdf"
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
<label className="flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={!!current.multiple}
onChange={(e) => patch({ multiple: e.target.checked })}
/>
<span> </span>
</label>
</>
)}
{(type === "number" || type === "date" || type === "datetime") && (
<div>
<label className="text-muted-foreground mb-1 block text-[0.62rem] font-semibold tracking-wider uppercase">
(format)
</label>
<input
type="text"
value={current.format || ""}
onChange={(e) => patch({ format: e.target.value || undefined })}
placeholder={type === "number" ? "#,##0" : "YYYY-MM-DD"}
className="border-border bg-background w-full rounded border px-2 py-1 text-xs"
/>
</div>
)}
</div>
);
};
export default InputConfigPanel;
@@ -0,0 +1,376 @@
"use client";
/**
* InvInputConfigPanel — 통합 "입력 필드" (id: input) cp 톤 설정 패널
*
* 흐름:
* ① 필드 타입 — CPVisualGrid 10칸 (text/number/date/datetime/select/entity/checkbox/textarea/file/code)
* ② 라벨 / 안내 — 라벨 + placeholder + 도움말
* ③ 타입별 옵션 — type 에 따라 조건부 (number min/max/step, text minLength/maxLength, textarea rows, select options, file accept/multiple, format)
* ▾ 옵션 — required / editable / disabled (FeatureChipGrid)
*
* Reference: notes/gbpark/2026-04-28-cp-panel-standard.md
*/
import React from "react";
import {
Type,
Hash,
Calendar,
CalendarClock,
ChevronDown,
Link2,
CheckSquare,
AlignLeft,
Paperclip,
Code2,
Plus,
X,
} from "lucide-react";
import {
CPSection,
CPRow,
CPGroup,
CPText,
CPNumber,
CPVisualGrid,
FeatureChipGrid,
Hint,
} from "@/components/v2/config-panels/_shared/cp";
import type { InputConfig, InputFieldType } from "./types";
export interface InvInputConfigPanelProps {
config?: InputConfig;
onChange?: (config: InputConfig) => void;
selectedComponent?: { id: string; config?: InputConfig; [k: string]: any };
}
const TYPE_OPTIONS: Array<{
value: InputFieldType;
label: string;
icon: React.ReactNode;
desc: string;
}> = [
{ value: "text", label: "문자열", icon: <Type size={16} />, desc: "일반 텍스트 한 줄" },
{ value: "number", label: "숫자", icon: <Hash size={16} />, desc: "정수 / 소수 입력" },
{ value: "date", label: "날짜", icon: <Calendar size={16} />, desc: "YYYY-MM-DD" },
{ value: "datetime", label: "날짜+시간", icon: <CalendarClock size={16} />, desc: "분/초까지" },
{ value: "select", label: "드롭다운", icon: <ChevronDown size={16} />, desc: "선택지 list" },
{ value: "entity", label: "엔티티", icon: <Link2 size={16} />, desc: "FK 참조 (팝업)" },
{ value: "checkbox", label: "체크박스", icon: <CheckSquare size={16} />, desc: "참/거짓" },
{ value: "textarea", label: "장문", icon: <AlignLeft size={16} />, desc: "여러 줄 텍스트" },
{ value: "file", label: "파일", icon: <Paperclip size={16} />, desc: "업로드" },
{ value: "code", label: "자동채번", icon: <Code2 size={16} />, desc: "readonly · 자동 생성" },
];
export const InvInputConfigPanel: React.FC<InvInputConfigPanelProps> = ({
config,
onChange,
selectedComponent,
}) => {
const current: InputConfig =
(config as InputConfig) || (selectedComponent?.config as InputConfig) || {};
const patch = (p: Partial<InputConfig>) => onChange?.({ ...current, ...p });
const type: InputFieldType = (current.type as InputFieldType) || "text";
const optionsArr: string[] = Array.isArray(current.options)
? current.options.map((o: any) => (typeof o === "string" ? o : o.label || o.value || ""))
: [];
const addOption = () => patch({ options: [...optionsArr, ""] as any });
const updateOption = (i: number, v: string) =>
patch({ options: optionsArr.map((o, idx) => (idx === i ? v : o)) as any });
const removeOption = (i: number) =>
patch({ options: optionsArr.filter((_, idx) => idx !== i) as any });
return (
<div style={{ fontFamily: "var(--v5-font-sans)", color: "var(--cp-text)", padding: "0 12px" }}>
{/* ── ① 필드 타입 ─────────────────────────── */}
<CPSection title="① 필드 타입" desc="입력 모드 (변형 결정)">
<CPVisualGrid
cols={5}
cardHeight={62}
value={type}
onChange={(v) => patch({ type: v as InputFieldType })}
options={TYPE_OPTIONS.map((o) => ({
value: o.value,
label: o.label,
preview: o.icon,
desc: o.desc,
}))}
/>
</CPSection>
{/* ── ② 라벨 / 안내 ─────────────────────────── */}
<CPSection title="② 라벨 / 안내">
<CPRow label="라벨" help="필드 위에 표시될 이름">
<CPText
value={current.label || ""}
onChange={(v) => patch({ label: v })}
placeholder="필드 라벨"
/>
</CPRow>
<CPRow label="placeholder" help="입력 전 안내 텍스트">
<CPText
value={current.placeholder || ""}
onChange={(v) => patch({ placeholder: v })}
placeholder="입력하세요"
/>
</CPRow>
<CPRow label="도움말" help="필드 하단 보조 안내">
<CPText
value={current.helperText || ""}
onChange={(v) => patch({ helperText: v || undefined })}
placeholder="하단 도움말"
/>
</CPRow>
</CPSection>
{/* ── ③ 타입별 옵션 (조건부) ─────────────────────────── */}
{type === "number" && (
<CPSection title="③ 숫자 옵션" desc="범위 / 증감">
<CPRow label="최소 (min)">
<CPNumber
value={current.min ?? undefined}
onChange={(v) => patch({ min: v })}
placeholder="제한 없음"
/>
</CPRow>
<CPRow label="최대 (max)">
<CPNumber
value={current.max ?? undefined}
onChange={(v) => patch({ max: v })}
placeholder="제한 없음"
/>
</CPRow>
<CPRow label="증감 단위 (step)" help="화살표/스피너 한 칸">
<CPNumber
value={current.step ?? undefined}
onChange={(v) => patch({ step: v })}
placeholder="1"
/>
</CPRow>
<CPRow label="포맷" help="예: #,##0 / 0.00">
<CPText
value={current.format || ""}
onChange={(v) => patch({ format: v || undefined })}
placeholder="#,##0"
/>
</CPRow>
</CPSection>
)}
{(type === "text" || type === "textarea") && (
<CPSection title="③ 문자열 옵션">
<CPRow label="최소 길이 (minLength)">
<CPNumber
value={current.minLength ?? undefined}
onChange={(v) => patch({ minLength: v })}
min={0}
placeholder="제한 없음"
/>
</CPRow>
<CPRow label="최대 길이 (maxLength)">
<CPNumber
value={current.maxLength ?? undefined}
onChange={(v) => patch({ maxLength: v })}
min={0}
placeholder="제한 없음"
/>
</CPRow>
{type === "textarea" && (
<CPRow label="줄 수 (rows)">
<CPNumber
value={current.rows ?? 3}
onChange={(v) => patch({ rows: v ?? 3 })}
min={1}
max={20}
/>
</CPRow>
)}
</CPSection>
)}
{(type === "date" || type === "datetime") && (
<CPSection title="③ 날짜 옵션">
<CPRow label="포맷" help={type === "date" ? "예: YYYY-MM-DD" : "예: YYYY-MM-DD HH:mm"}>
<CPText
value={current.format || ""}
onChange={(v) => patch({ format: v || undefined })}
placeholder={type === "date" ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm"}
/>
</CPRow>
</CPSection>
)}
{type === "select" && (
<CPSection title="③ 선택지" desc={`${optionsArr.length}`}>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 5 }}>
<button
type="button"
onClick={addOption}
style={{
padding: "4px 10px",
fontSize: 10.5,
background: "var(--cp-bg-subtle)",
border: "1px solid var(--cp-border)",
borderRadius: 4,
cursor: "pointer",
color: "var(--cp-text)",
fontFamily: "var(--v5-font-sans)",
display: "inline-flex",
alignItems: "center",
gap: 4,
}}
>
<Plus size={10} />
</button>
</div>
{optionsArr.length === 0 ? (
<Hint> . [+ ] .</Hint>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{optionsArr.map((opt, i) => (
<div
key={i}
style={{
display: "grid",
gridTemplateColumns: "16px 1fr 22px",
alignItems: "center",
columnGap: 6,
padding: "3px 6px",
background: "var(--cp-bg-subtle)",
border: "1px solid var(--cp-border-subtle)",
borderRadius: 4,
}}
>
<span
style={{
fontSize: 9,
color: "var(--cp-text-muted)",
fontFamily: "var(--v5-font-mono)",
textAlign: "right",
}}
>
{i + 1}
</span>
<input
type="text"
value={opt}
onChange={(e) => updateOption(i, e.target.value)}
placeholder={`옵션 ${i + 1}`}
style={{
height: 22,
padding: "0 6px",
fontSize: 11,
background: "var(--cp-surface)",
border: "1px solid var(--cp-border)",
borderRadius: 3,
color: "var(--cp-text)",
outline: "none",
fontFamily: "var(--v5-font-sans)",
}}
/>
<button
type="button"
onClick={() => removeOption(i)}
style={{
width: 22,
height: 22,
padding: 0,
background: "transparent",
border: "none",
cursor: "pointer",
color: "var(--v5-red, #ef4444)",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 4,
}}
>
<X size={10} />
</button>
</div>
))}
</div>
)}
</CPSection>
)}
{type === "file" && (
<CPSection title="③ 파일 옵션">
<CPRow label="허용 확장자" help="예: image/* 또는 .pdf,.docx">
<CPText
value={current.accept || ""}
onChange={(v) => patch({ accept: v || undefined })}
placeholder="image/*"
/>
</CPRow>
<FeatureChipGrid
items={[
{
key: "multiple",
label: "다중 선택",
desc: "한 번에 여러 파일을 선택할 수 있어요.\nOFF 일 때는 한 파일씩 교체.",
},
]}
source={current as any}
onToggle={(k, v) => patch({ [k]: v } as Partial<InputConfig>)}
/>
</CPSection>
)}
{type === "entity" && (
<CPSection title="③ 엔티티 참조" desc="FK 참조 — 팝업에서 행 선택">
<Hint tone="warn">
FK picker UI . type ( ).
</Hint>
</CPSection>
)}
{type === "code" && (
<CPSection title="③ 자동채번" desc="readonly · 저장 시 자동 생성">
<Hint> .</Hint>
</CPSection>
)}
{type === "checkbox" && (
<CPSection title="③ 체크박스">
<Hint> (/).</Hint>
</CPSection>
)}
{/* ── ▾ 옵션 ─────────────────────────── */}
<CPGroup title="옵션" defaultOpen>
<FeatureChipGrid
items={[
{
key: "required",
label: "필수",
desc: "비어 있으면 저장 시 검증 오류.\n라벨 우측에 빨간 * 표시.",
},
{
key: "editable",
label: "편집 가능",
default: true,
desc: "사용자가 값을 수정할 수 있어요.\nOFF = 화면에는 보이되 편집 불가 (readonly).",
},
{
key: "disabled",
label: "비활성화",
desc: "필드 자체를 회색 처리하고 입력/포커스 차단.\n조건부 표시와 함께 자주 쓰여요.",
},
]}
source={current as any}
onToggle={(k, v) => patch({ [k]: v } as Partial<InputConfig>)}
/>
</CPGroup>
</div>
);
};
InvInputConfigPanel.displayName = "InvInputConfigPanel";
export default InvInputConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { InputWrapper } from "./InputComponent";
import { InputConfigPanel } from "./InputConfigPanel";
import { InvInputConfigPanel } from "./InvInputConfigPanel";
import type { InputConfig } from "./types";
/**
@@ -42,7 +42,7 @@ export const InputDefinition = createComponentDefinition({
component: InputWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 240, height: 48 },
config_panel: InputConfigPanel,
config_panel: InvInputConfigPanel,
icon: "Edit",
tags: ["입력", "input", "field", "text", "number", "date", "select"],
version: "2.0.0",
@@ -61,4 +61,4 @@ export const InputDefinition = createComponentDefinition({
export type { InputConfig } from "./types";
export { InputComponent, InputWrapper } from "./InputComponent";
export { InputConfigPanel } from "./InputConfigPanel";
export { InvInputConfigPanel } from "./InvInputConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* SearchConfigPanel "검색" (id: search) cp
* InvSearchConfigPanel "검색" (id: search) cp
*
* :
* +
@@ -28,7 +28,7 @@ import {
} from "@/components/v2/config-panels/_shared/cp";
import type { SearchConfig } from "./types";
export interface SearchConfigPanelProps {
export interface InvSearchConfigPanelProps {
config?: SearchConfig;
onChange?: (config: SearchConfig) => void;
selectedComponent?: { id: string; config?: SearchConfig; [k: string]: any };
@@ -58,7 +58,7 @@ const FIELD_TYPE_LABEL: Record<SearchField["type"], string> = {
select: "선택",
};
export const SearchConfigPanel: React.FC<SearchConfigPanelProps> = ({
export const InvSearchConfigPanel: React.FC<InvSearchConfigPanelProps> = ({
config,
onChange,
selectedComponent,
@@ -287,7 +287,7 @@ export const SearchConfigPanel: React.FC<SearchConfigPanelProps> = ({
);
};
SearchConfigPanel.displayName = "SearchConfigPanel";
InvSearchConfigPanel.displayName = "InvSearchConfigPanel";
// ───────────────────────────────────────────────────────
// SearchFieldRow — Dense list 한 줄 (높이 ~24px, 카드 박스 폐기)
@@ -424,4 +424,4 @@ function SearchFieldRow({
);
}
export default SearchConfigPanel;
export default InvSearchConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { SearchWrapper } from "./SearchComponent";
import { SearchConfigPanel } from "./SearchConfigPanel";
import { InvSearchConfigPanel } from "./InvSearchConfigPanel";
import type { SearchConfig } from "./types";
/**
@@ -40,7 +40,7 @@ export const SearchDefinition = createComponentDefinition({
component: SearchWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 480, height: 48 },
config_panel: SearchConfigPanel,
config_panel: InvSearchConfigPanel,
icon: "Search",
tags: ["검색", "search", "filter", "widget"],
version: "2.0.0",
@@ -55,4 +55,4 @@ export const SearchDefinition = createComponentDefinition({
export type { SearchConfig } from "./types";
export { SearchComponent, SearchWrapper } from "./SearchComponent";
export { SearchConfigPanel } from "./SearchConfigPanel";
export { InvSearchConfigPanel } from "./InvSearchConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* StatsConfigPanel "통계 카드" (id: stats) cp
* InvStatsConfigPanel "통계 카드" (id: stats) cp
*
* :
*
@@ -37,7 +37,7 @@ const COLOR_PRESETS = [
{ id: "dark", name: "다크", colors: ["#1e3a5f", "#1e3f28", "#5c3c0a", "#5c1a1a"] },
];
export interface StatsConfigPanelProps {
export interface InvStatsConfigPanelProps {
config?: StatsConfig;
onChange?: (config: StatsConfig) => void;
selectedComponent?: { id: string; config?: StatsConfig; [k: string]: any };
@@ -47,7 +47,7 @@ export interface StatsConfigPanelProps {
onTableChange?: (tableName: string) => void;
}
export const StatsConfigPanel: React.FC<StatsConfigPanelProps> = ({
export const InvStatsConfigPanel: React.FC<InvStatsConfigPanelProps> = ({
config,
onChange,
selectedComponent,
@@ -346,7 +346,7 @@ export const StatsConfigPanel: React.FC<StatsConfigPanelProps> = ({
);
};
StatsConfigPanel.displayName = "StatsConfigPanel";
InvStatsConfigPanel.displayName = "InvStatsConfigPanel";
// ───────────────────────────────────────────────────────
// CardPreview / ChipPreview / BigNumberPreview — 표시 스타일 미리보기
@@ -625,4 +625,4 @@ function inputStyle({ mono = false }: { mono?: boolean } = {}): React.CSSPropert
};
}
export default StatsConfigPanel;
export default InvStatsConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { StatsWrapper } from "./StatsComponent";
import { StatsConfigPanel } from "./StatsConfigPanel";
import { InvStatsConfigPanel } from "./InvStatsConfigPanel";
import type { StatsConfig } from "./types";
/**
@@ -46,7 +46,7 @@ export const StatsDefinition = createComponentDefinition({
component: StatsWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 480, height: 120 },
config_panel: StatsConfigPanel,
config_panel: InvStatsConfigPanel,
icon: "BarChart",
tags: ["통계", "kpi", "stats", "aggregation", "card"],
version: "2.0.0",
@@ -61,4 +61,4 @@ export const StatsDefinition = createComponentDefinition({
export type { StatsConfig } from "./types";
export { StatsComponent, StatsWrapper } from "./StatsComponent";
export { StatsConfigPanel } from "./StatsConfigPanel";
export { InvStatsConfigPanel } from "./InvStatsConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* TableConfigPanel "테이블" (id: table) cp
* InvTableConfigPanel "테이블" (id: table) cp
*
* :
* + DB
@@ -43,7 +43,7 @@ import {
} from "@/components/v2/config-panels/_shared/cp";
import type { TableConfig, TableColumn } from "./types";
export interface TableConfigPanelProps {
export interface InvTableConfigPanelProps {
config?: TableConfig;
onChange?: (config: TableConfig) => void;
selectedComponent?: { id: string; config?: TableConfig; [k: string]: any };
@@ -53,7 +53,7 @@ export interface TableConfigPanelProps {
onTableChange?: (tableName: string) => void;
}
export const TableConfigPanel: React.FC<TableConfigPanelProps> = ({
export const InvTableConfigPanel: React.FC<InvTableConfigPanelProps> = ({
config,
onChange,
selectedComponent,
@@ -470,7 +470,7 @@ export const TableConfigPanel: React.FC<TableConfigPanelProps> = ({
);
};
TableConfigPanel.displayName = "TableConfigPanel";
InvTableConfigPanel.displayName = "InvTableConfigPanel";
// ───────────────────────────────────────────────────────
// ColumnEditRow — 컬럼 한 줄 편집 (dense)
@@ -652,4 +652,4 @@ function inputStyle({ mono = false }: { mono?: boolean } = {}): React.CSSPropert
};
}
export default TableConfigPanel;
export default InvTableConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { TableWrapper } from "./TableComponent";
import { TableConfigPanel } from "./TableConfigPanel";
import { InvTableConfigPanel } from "./InvTableConfigPanel";
import type { TableConfig } from "./types";
/**
@@ -49,7 +49,7 @@ export const TableDefinition = createComponentDefinition({
component: TableWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 800, height: 400 },
config_panel: TableConfigPanel,
config_panel: InvTableConfigPanel,
icon: "Table",
tags: ["테이블", "table", "grid", "list", "data", "split", "pivot"],
version: "2.0.0",
@@ -71,4 +71,4 @@ export const TableDefinition = createComponentDefinition({
export type { TableConfig, TableColumn } from "./types";
export { TableComponent, TableWrapper } from "./TableComponent";
export { TableConfigPanel } from "./TableConfigPanel";
export { InvTableConfigPanel } from "./InvTableConfigPanel";
@@ -1,7 +1,7 @@
"use client";
/**
* TitleConfigPanel "제목/텍스트" (id: title) cp
* InvTitleConfigPanel "제목/텍스트" (id: title) cp
*
* :
*
@@ -28,14 +28,14 @@ import {
} from "@/components/v2/config-panels/_shared/cp";
import type { TitleConfig } from "./types";
export interface TitleConfigPanelProps {
export interface InvTitleConfigPanelProps {
config?: TitleConfig;
onChange?: (config: TitleConfig) => void;
onUpdateProperty?: (componentId: string, path: string, value: unknown) => void;
selectedComponent?: { id: string; config?: TitleConfig; [k: string]: any };
}
export const TitleConfigPanel: React.FC<TitleConfigPanelProps> = ({
export const InvTitleConfigPanel: React.FC<InvTitleConfigPanelProps> = ({
config,
onChange,
onUpdateProperty,
@@ -199,6 +199,6 @@ export const TitleConfigPanel: React.FC<TitleConfigPanelProps> = ({
);
};
TitleConfigPanel.displayName = "TitleConfigPanel";
InvTitleConfigPanel.displayName = "InvTitleConfigPanel";
export default TitleConfigPanel;
export default InvTitleConfigPanel;
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { TitleWrapper } from "./TitleComponent";
import { TitleConfigPanel } from "./TitleConfigPanel";
import { InvTitleConfigPanel } from "./InvTitleConfigPanel";
import type { TitleConfig } from "./types";
/**
@@ -41,7 +41,7 @@ export const TitleDefinition = createComponentDefinition({
component: TitleWrapper,
default_config: DEFAULT_CONFIG as Record<string, any>,
default_size: { width: 200, height: 28 },
config_panel: TitleConfigPanel,
config_panel: InvTitleConfigPanel,
icon: "Type",
tags: ["제목", "텍스트", "title", "text", "label", "heading"],
version: "2.0.0",
@@ -52,4 +52,4 @@ export const TitleDefinition = createComponentDefinition({
export type { TitleConfig } from "./types";
export { TitleComponent, TitleWrapper } from "./TitleComponent";
export { TitleConfigPanel } from "./TitleConfigPanel";
export { InvTitleConfigPanel } from "./InvTitleConfigPanel";
@@ -3,7 +3,7 @@
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import { ButtonPrimaryWrapper } from "./ButtonPrimaryComponent";
import { InvButtonConfigPanel } from "@/components/v2/config-panels/InvButtonConfigPanel";
import { InvLegacyButtonConfigPanel } from "@/components/v2/config-panels/InvLegacyButtonConfigPanel";
import { withContainerQuery } from "../../hoc/withContainerQuery";
/**
@@ -45,7 +45,7 @@ export const V2ButtonPrimaryDefinition = createComponentDefinition({
},
},
default_size: { width: 100, height: 40 },
config_panel: InvButtonConfigPanel,
config_panel: InvLegacyButtonConfigPanel,
icon: "MousePointer",
tags: ["버튼", "액션", "클릭"],
version: "1.0.0",
@@ -5,7 +5,7 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition
import { ComponentCategory } from "@/types/component";
import type { WebType } from "@/types/screen";
import { DividerLineWrapper } from "./DividerLineComponent";
import { InvDividerConfigPanel } from "@/components/v2/config-panels/InvDividerConfigPanel";
import { InvLegacyDividerConfigPanel } from "@/components/v2/config-panels/InvLegacyDividerConfigPanel";
import { DividerLineConfig } from "./types";
/**
@@ -25,7 +25,7 @@ export const V2DividerLineDefinition = createComponentDefinition({
maxLength: 255,
},
default_size: { width: 400, height: 2 },
config_panel: InvDividerConfigPanel,
config_panel: InvLegacyDividerConfigPanel,
icon: "Layout",
tags: [],
version: "1.0.0",
@@ -6,7 +6,7 @@
import { ComponentCategory } from "@/types/component";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { V2FieldConfigPanel } from "@/components/v2/config-panels/V2FieldConfigPanel";
import { InvFieldConfigPanel } from "@/components/v2/config-panels/InvFieldConfigPanel";
import { V2Input } from "@/components/v2/V2Input";
import { withContainerQuery } from "../../hoc/withContainerQuery";
@@ -29,7 +29,7 @@ export const V2InputDefinition = createComponentDefinition({
tags: ["input", "text", "number", "v2"],
// 설정 패널
config_panel: V2FieldConfigPanel,
config_panel: InvFieldConfigPanel,
// ─── INVYONE DataPort 선언 ───
dataPorts: {
@@ -6,7 +6,7 @@
import { ComponentCategory } from "@/types/component";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { V2FieldConfigPanel } from "@/components/v2/config-panels/V2FieldConfigPanel";
import { InvFieldConfigPanel } from "@/components/v2/config-panels/InvFieldConfigPanel";
import { V2Select } from "@/components/v2/V2Select";
import { withContainerQuery } from "../../hoc/withContainerQuery";
@@ -38,7 +38,7 @@ export const V2SelectDefinition = createComponentDefinition({
tags: ["select", "dropdown", "combobox", "v2"],
// 설정 패널
config_panel: V2FieldConfigPanel,
config_panel: InvFieldConfigPanel,
});
export default V2SelectDefinition;
@@ -5,7 +5,7 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition
import { ComponentCategory } from "@/types/component";
import type { WebType } from "@/types/screen";
import { TextDisplayWrapper } from "./TextDisplayComponent";
import { InvTextConfigPanel } from "@/components/v2/config-panels/InvTextConfigPanel";
import { InvLegacyTextConfigPanel } from "@/components/v2/config-panels/InvLegacyTextConfigPanel";
import { TextDisplayConfig } from "./types";
import { withContainerQuery } from "../../hoc/withContainerQuery";
@@ -29,7 +29,7 @@ export const V2TextDisplayDefinition = createComponentDefinition({
textAlign: "left",
},
default_size: { width: 150, height: 24 },
config_panel: InvTextConfigPanel,
config_panel: InvLegacyTextConfigPanel,
icon: "Type",
tags: ["텍스트", "표시", "라벨"],
version: "1.0.0",
+23 -17
View File
@@ -13,18 +13,18 @@ import type { ConfigPanelContext } from "@/lib/registry/components/common/Config
// 관련 문서: notes/gbpark/2026-04-11-component-unification-plan.md §10.5
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
// ========== INVYONE 통합 컴포넌트 (2026-04-11, Phase A~) ==========
"divider": () => import("@/lib/registry/components/divider/DividerConfigPanel"),
"title": () => import("@/lib/registry/components/title/TitleConfigPanel"),
"button": () => import("@/lib/registry/components/button/ButtonConfigPanel"),
"search": () => import("@/lib/registry/components/search/SearchConfigPanel"),
"input": () => import("@/lib/registry/components/input/InputConfigPanel"),
"stats": () => import("@/lib/registry/components/stats/StatsConfigPanel"),
"table": () => import("@/lib/registry/components/table/TableConfigPanel"),
"container": () => import("@/lib/registry/components/container/ContainerConfigPanel"),
"divider": () => import("@/lib/registry/components/divider/InvDividerConfigPanel"),
"title": () => import("@/lib/registry/components/title/InvTitleConfigPanel"),
"button": () => import("@/lib/registry/components/button/InvButtonConfigPanel"),
"search": () => import("@/lib/registry/components/search/InvSearchConfigPanel"),
"input": () => import("@/lib/registry/components/input/InvInputConfigPanel"),
"stats": () => import("@/lib/registry/components/stats/InvStatsConfigPanel"),
"table": () => import("@/lib/registry/components/table/InvTableConfigPanel"),
"container": () => import("@/lib/registry/components/container/InvContainerConfigPanel"),
// ========== V2 컴포넌트 ==========
"v2-input": () => import("@/components/v2/config-panels/V2FieldConfigPanel"),
"v2-select": () => import("@/components/v2/config-panels/V2FieldConfigPanel"),
"v2-input": () => import("@/components/v2/config-panels/InvFieldConfigPanel"),
"v2-select": () => import("@/components/v2/config-panels/InvFieldConfigPanel"),
"v2-date": () => import("@/components/v2/config-panels/V2DateConfigPanel"),
"v2-list": () => import("@/components/v2/config-panels/InvDataConfigPanel"),
"v2-media": () => import("@/components/v2/config-panels/V2MediaConfigPanel"),
@@ -49,14 +49,17 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
// ========== 버튼 ==========
"button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
"v2-button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
// v2-button-primary: hidden 호환 — InvLegacy 패널 사용 (옛 화면 config 스키마 보존)
"v2-button-primary": () => import("@/components/v2/config-panels/InvLegacyButtonConfigPanel"),
// ========== 표시 컴포넌트 ==========
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
"v2-text-display": () => import("@/lib/registry/components/v2-text-display/TextDisplayConfigPanel"),
// v2-text-display: hidden 호환 — InvLegacy 패널 사용
"v2-text-display": () => import("@/components/v2/config-panels/InvLegacyTextConfigPanel"),
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
"v2-divider-line": () => import("@/lib/registry/components/v2-divider-line/DividerLineConfigPanel"),
// v2-divider-line: hidden 호환 — InvLegacy 패널 사용
"v2-divider-line": () => import("@/components/v2/config-panels/InvLegacyDividerConfigPanel"),
"image-widget": () => import("@/lib/registry/components/image-widget/ImageWidgetConfigPanel"),
// ========== 레이아웃/컨테이너 ==========
@@ -123,9 +126,9 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"v2-bom-tree": () => import("@/components/v2/config-panels/V2BomTreeConfigPanel"),
// ========== 레거시 위젯 (component/onUpdateProperty props 사용) ==========
// ★ "stats" key 는 위 INVYONE 통합 섹션에서 정의됨 — 여기 중복 정의 금지 (덮어씀 → 통합 패널 안 보임)
"card": () => import("@/components/screen/config-panels/CardConfigPanel"),
"dashboard": () => import("@/components/screen/config-panels/DashboardConfigPanel"),
"stats": () => import("@/components/screen/config-panels/StatsCardConfigPanel"),
"stats-card": () => import("@/components/screen/config-panels/StatsCardConfigPanel"),
"progress": () => import("@/components/screen/config-panels/ProgressBarConfigPanel"),
"progress-bar": () => import("@/components/screen/config-panels/ProgressBarConfigPanel"),
@@ -143,10 +146,13 @@ const configPanelCache = new Map<string, React.ComponentType<any>>();
* 컴포넌트 ID로 ConfigPanel 컴포넌트를 동적으로 로드
*/
// ── Phase E: v2-* → 통합 컴포넌트 ConfigPanel alias ──
// ★ v2-button-primary / v2-divider-line / v2-text-display 는 alias 제외:
// hidden 호환 컴포넌트의 옛 화면 config 스키마가 통합 컴포넌트와 다르므로
// InvLegacy 패널 (위 CONFIG_PANEL_MAP) 로 직접 매핑되어야 함.
const CONFIG_PANEL_ALIAS: Record<string, string> = {
"v2-divider-line": "divider", "divider-line": "divider", "v2-split-line": "divider",
"v2-text-display": "title", "text-display": "title",
"v2-button-primary": "button", "button-primary": "button",
"divider-line": "divider", "v2-split-line": "divider",
"text-display": "title",
"button-primary": "button",
"v2-table-search-widget": "search", "table-search-widget": "search",
"v2-input": "input", "v2-select": "input", "v2-date": "input",
"text-input": "input", "number-input": "input", "date-input": "input",