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>
656 lines
22 KiB
TypeScript
656 lines
22 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* InvTableConfigPanel — 통합 "테이블" (id: table) cp 톤 설정 패널
|
||
*
|
||
* 흐름:
|
||
* ① 테이블 연결 — 테이블 선택 + DB 컬럼 자동 로드
|
||
* ② 표시 모드 — CPVisualGrid 5칸 (기본/분할/그룹/피벗/카드 시각 미리보기)
|
||
* ③ 행 선택 + 높이 — CPSegment
|
||
* ④ 모드별 설정 — split 비율 / grouped 컬럼 (조건부)
|
||
* ⑤ 컬럼 편집 — dense list (한 줄 = 라벨 + key + 너비 + align + 정렬)
|
||
* ▾ 동작 옵션 — 헤더/푸터/체크박스/줄무늬/호버/툴바 (FeatureChipGrid)
|
||
*
|
||
* Reference: notes/gbpark/2026-04-28-cp-panel-standard.md
|
||
*/
|
||
|
||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||
import {
|
||
Table2,
|
||
Columns2,
|
||
List,
|
||
Grid3x3,
|
||
LayoutGrid,
|
||
Plus,
|
||
Wand2,
|
||
AlignLeft,
|
||
AlignCenter,
|
||
AlignRight,
|
||
} from "lucide-react";
|
||
import {
|
||
CPSection,
|
||
CPRow,
|
||
CPGroup,
|
||
CPText,
|
||
CPSelect,
|
||
CPSegment,
|
||
CPNumber,
|
||
CPSwitch,
|
||
CPVisualGrid,
|
||
FeatureChipGrid,
|
||
Hint,
|
||
InlineLoader,
|
||
} from "@/components/v2/config-panels/_shared/cp";
|
||
import type { TableConfig, TableColumn } from "./types";
|
||
|
||
export interface InvTableConfigPanelProps {
|
||
config?: TableConfig;
|
||
onChange?: (config: TableConfig) => void;
|
||
selectedComponent?: { id: string; config?: TableConfig; [k: string]: any };
|
||
tables?: any[];
|
||
tableColumns?: any[];
|
||
screenTableName?: string;
|
||
onTableChange?: (tableName: string) => void;
|
||
}
|
||
|
||
export const InvTableConfigPanel: React.FC<InvTableConfigPanelProps> = ({
|
||
config,
|
||
onChange,
|
||
selectedComponent,
|
||
tables,
|
||
tableColumns,
|
||
screenTableName,
|
||
onTableChange,
|
||
}) => {
|
||
const current: TableConfig =
|
||
(config as TableConfig) || (selectedComponent?.config as TableConfig) || {};
|
||
|
||
const patch = useCallback(
|
||
(p: Partial<TableConfig>) => onChange?.({ ...current, ...p }),
|
||
[current, onChange],
|
||
);
|
||
|
||
const columns: TableColumn[] = current.columns ?? [];
|
||
const connectedTable = (current as any).selectedTable || screenTableName;
|
||
|
||
// ── 전체 DB 테이블 직접 로드 ──
|
||
const [allDbTables, setAllDbTables] = useState<any[]>([]);
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
(async () => {
|
||
try {
|
||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||
const res = await tableManagementApi.getTableList();
|
||
if (!cancelled && res.success && res.data) setAllDbTables(res.data);
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, []);
|
||
|
||
const tableOptions = useMemo(() => {
|
||
const src = allDbTables.length > 0 ? allDbTables : tables || [];
|
||
return src.map((t: any) => ({
|
||
value: t.tableName || t.table_name,
|
||
label:
|
||
t.display_name || t.tableLabel || t.table_label || t.tableName || t.table_name,
|
||
}));
|
||
}, [allDbTables, tables]);
|
||
|
||
// ── 연결된 테이블의 컬럼 로드 (자동 로드 button 용) ──
|
||
const [connectedTableColumns, setConnectedTableColumns] = useState<any[]>([]);
|
||
const [loadingConnectedColumns, setLoadingConnectedColumns] = useState(false);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
if (!connectedTable) {
|
||
setConnectedTableColumns([]);
|
||
setLoadingConnectedColumns(false);
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}
|
||
if (
|
||
connectedTable === screenTableName &&
|
||
Array.isArray(tableColumns) &&
|
||
tableColumns.length > 0
|
||
) {
|
||
setConnectedTableColumns(tableColumns);
|
||
setLoadingConnectedColumns(false);
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}
|
||
setLoadingConnectedColumns(true);
|
||
(async () => {
|
||
try {
|
||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||
const cols = await tableTypeApi.getColumns(connectedTable, true);
|
||
if (!cancelled) setConnectedTableColumns(Array.isArray(cols) ? cols : []);
|
||
} catch {
|
||
if (!cancelled) setConnectedTableColumns([]);
|
||
} finally {
|
||
if (!cancelled) setLoadingConnectedColumns(false);
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [connectedTable, screenTableName, tableColumns]);
|
||
|
||
const effectiveTableColumns = useMemo(() => {
|
||
if (
|
||
connectedTable === screenTableName &&
|
||
Array.isArray(tableColumns) &&
|
||
tableColumns.length > 0
|
||
) {
|
||
return tableColumns;
|
||
}
|
||
return connectedTableColumns;
|
||
}, [connectedTable, screenTableName, tableColumns, connectedTableColumns]);
|
||
|
||
const autoLoadColumns = useCallback(() => {
|
||
if (!effectiveTableColumns?.length) return;
|
||
const newCols: TableColumn[] = effectiveTableColumns.map((col: any) => ({
|
||
key: col.columnName || col.column_name,
|
||
label:
|
||
col.columnLabel ||
|
||
col.column_label ||
|
||
col.displayName ||
|
||
col.columnName ||
|
||
col.column_name,
|
||
width: undefined,
|
||
align: "left" as const,
|
||
sortable: true,
|
||
visible: true,
|
||
}));
|
||
patch({ columns: newCols });
|
||
}, [effectiveTableColumns, patch]);
|
||
|
||
const updateColumn = (idx: number, col: Partial<TableColumn>) => {
|
||
const next = columns.map((c, i) => (i === idx ? { ...c, ...col } : c));
|
||
patch({ columns: next });
|
||
};
|
||
|
||
const addColumn = () => {
|
||
patch({
|
||
columns: [
|
||
...columns,
|
||
{ key: `col${columns.length + 1}`, label: `컬럼 ${columns.length + 1}` },
|
||
],
|
||
});
|
||
};
|
||
|
||
const removeColumn = (idx: number) => {
|
||
patch({ columns: columns.filter((_, i) => i !== idx) });
|
||
};
|
||
|
||
const displayMode = current.displayMode || "table";
|
||
|
||
return (
|
||
<div style={{ fontFamily: "var(--v5-font-sans)", color: "var(--cp-text)", padding: "0 12px" }}>
|
||
{/* ── ① 테이블 연결 ─────────────────────────── */}
|
||
<CPSection title="① 테이블 연결" desc="DB 테이블 매핑">
|
||
<CPRow label="테이블">
|
||
<CPSelect
|
||
value={connectedTable || ""}
|
||
onChange={(v) => {
|
||
onTableChange?.(v);
|
||
patch({ selectedTable: v, columns: [] } as any);
|
||
}}
|
||
>
|
||
<option value="">선택...</option>
|
||
{tableOptions.map((t) => (
|
||
<option key={t.value} value={t.value}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</CPSelect>
|
||
</CPRow>
|
||
|
||
{connectedTable && (
|
||
<Hint>
|
||
{loadingConnectedColumns
|
||
? "컬럼 정보를 불러오는 중..."
|
||
: effectiveTableColumns.length > 0
|
||
? `컬럼 ${effectiveTableColumns.length}개 준비됨`
|
||
: "불러온 컬럼 정보가 없습니다"}
|
||
</Hint>
|
||
)}
|
||
|
||
{connectedTable && effectiveTableColumns.length > 0 && (
|
||
<div style={{ marginTop: 6 }}>
|
||
<button
|
||
type="button"
|
||
onClick={autoLoadColumns}
|
||
style={{
|
||
width: "100%",
|
||
padding: "6px 10px",
|
||
fontSize: 10.5,
|
||
background: "rgba(var(--v5-primary-rgb), 0.08)",
|
||
border: "1px solid rgba(var(--v5-primary-rgb), 0.32)",
|
||
borderRadius: 4,
|
||
cursor: "pointer",
|
||
color: "var(--v5-primary, #6c5ce7)",
|
||
fontFamily: "var(--v5-font-sans)",
|
||
fontWeight: 600,
|
||
letterSpacing: "-0.005em",
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
gap: 6,
|
||
transition: "background .14s ease, border-color .14s ease",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
(e.currentTarget as HTMLButtonElement).style.background =
|
||
"rgba(var(--v5-primary-rgb), 0.14)";
|
||
(e.currentTarget as HTMLButtonElement).style.borderColor =
|
||
"rgba(var(--v5-primary-rgb), 0.5)";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
(e.currentTarget as HTMLButtonElement).style.background =
|
||
"rgba(var(--v5-primary-rgb), 0.08)";
|
||
(e.currentTarget as HTMLButtonElement).style.borderColor =
|
||
"rgba(var(--v5-primary-rgb), 0.32)";
|
||
}}
|
||
>
|
||
<Wand2 size={11} />
|
||
DB 컬럼에서 자동 로드 ({effectiveTableColumns.length}개)
|
||
</button>
|
||
</div>
|
||
)}
|
||
</CPSection>
|
||
|
||
{/* ── ② 표시 모드 ─────────────────────────── */}
|
||
<CPSection title="② 표시 모드" desc="테이블 형태 변형">
|
||
<CPVisualGrid
|
||
cols={5}
|
||
cardHeight={62}
|
||
value={displayMode}
|
||
onChange={(v) => patch({ displayMode: v as TableConfig["displayMode"] })}
|
||
options={[
|
||
{ value: "table", label: "기본", preview: <Table2 size={16} />, desc: "일반 테이블" },
|
||
{ value: "split", label: "분할", preview: <Columns2 size={16} />, desc: "좌우 분할" },
|
||
{ value: "grouped", label: "그룹", preview: <List size={16} />, desc: "그룹핑" },
|
||
{ value: "pivot", label: "피벗", preview: <Grid3x3 size={16} />, desc: "피벗 그리드" },
|
||
{ value: "card", label: "카드", preview: <LayoutGrid size={16} />, desc: "카드 리스트" },
|
||
]}
|
||
/>
|
||
</CPSection>
|
||
|
||
{/* ── ③ 행 선택 + 높이 ─────────────────────────── */}
|
||
<CPSection title="③ 행 / 높이">
|
||
<CPRow label="행 선택">
|
||
<CPSegment
|
||
value={current.selectionMode || "single"}
|
||
onChange={(v) => patch({ selectionMode: v as TableConfig["selectionMode"] })}
|
||
options={[
|
||
{ value: "none", label: "없음" },
|
||
{ value: "single", label: "단일" },
|
||
{ value: "multiple", label: "복수" },
|
||
]}
|
||
/>
|
||
</CPRow>
|
||
<CPRow label="행 높이">
|
||
<CPSegment
|
||
value={current.rowHeight || "normal"}
|
||
onChange={(v) => patch({ rowHeight: v as TableConfig["rowHeight"] })}
|
||
options={[
|
||
{ value: "compact", label: "좁게" },
|
||
{ value: "normal", label: "기본" },
|
||
{ value: "relaxed", label: "넓게" },
|
||
]}
|
||
/>
|
||
</CPRow>
|
||
</CPSection>
|
||
|
||
{/* ── ④ 모드별 설정 (조건부) ─────────────────────────── */}
|
||
{displayMode === "split" && (
|
||
<CPSection title="④ 분할 설정" desc="좌측 패널 비율">
|
||
<CPRow label="좌측 비율" help="0.1 ~ 0.9 (예: 0.5 = 절반)">
|
||
<CPNumber
|
||
value={current.splitRatio ?? 0.5}
|
||
onChange={(v) => patch({ splitRatio: v ?? 0.5 })}
|
||
min={0.1}
|
||
max={0.9}
|
||
step={0.05}
|
||
/>
|
||
</CPRow>
|
||
</CPSection>
|
||
)}
|
||
|
||
{displayMode === "grouped" && (
|
||
<CPSection title="④ 그룹 설정" desc="그룹화 기준 컬럼">
|
||
<CPRow label="그룹화 컬럼">
|
||
<CPSelect
|
||
value={current.groupBy || ""}
|
||
onChange={(v) => patch({ groupBy: v || undefined })}
|
||
searchable={false}
|
||
>
|
||
<option value="">선택...</option>
|
||
{columns.map((c) => (
|
||
<option key={c.key} value={c.key}>
|
||
{c.label || c.key}
|
||
</option>
|
||
))}
|
||
</CPSelect>
|
||
</CPRow>
|
||
{columns.length === 0 && (
|
||
<Hint tone="warn">컬럼을 먼저 자동 로드 또는 추가하세요.</Hint>
|
||
)}
|
||
</CPSection>
|
||
)}
|
||
|
||
{displayMode === "pivot" && (
|
||
<CPSection title="④ 피벗 설정" desc="개발 중">
|
||
<Hint tone="warn">
|
||
피벗 row/column/values 편집 UI는 추후 추가 예정.
|
||
<br />
|
||
현재는 displayMode 만 저장 (백엔드 키 보존).
|
||
</Hint>
|
||
</CPSection>
|
||
)}
|
||
|
||
{/* ── ⑤ 컬럼 ─────────────────────────── */}
|
||
<CPSection title="⑤ 컬럼" desc={`${columns.length}개`}>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "flex-end",
|
||
marginBottom: 5,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={addColumn}
|
||
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>
|
||
|
||
{columns.length === 0 ? (
|
||
<Hint>
|
||
{connectedTable
|
||
? loadingConnectedColumns
|
||
? "컬럼 정보를 불러오는 중입니다."
|
||
: effectiveTableColumns.length > 0
|
||
? "위 [자동 로드] 버튼으로 한 번에 가져올 수 있습니다."
|
||
: "연결된 테이블에서 컬럼 정보를 찾지 못했습니다."
|
||
: "테이블을 연결하면 자동 로드할 수 있습니다."}
|
||
</Hint>
|
||
) : (
|
||
<div
|
||
style={{
|
||
border: "1px solid var(--cp-border-subtle)",
|
||
borderRadius: 5,
|
||
overflow: "hidden",
|
||
background: "var(--cp-bg-subtle)",
|
||
}}
|
||
>
|
||
{columns.map((col, idx) => (
|
||
<ColumnEditRow
|
||
key={`${col.key}-${idx}`}
|
||
index={idx}
|
||
col={col}
|
||
isLast={idx === columns.length - 1}
|
||
onChange={(p) => updateColumn(idx, p)}
|
||
onRemove={() => removeColumn(idx)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CPSection>
|
||
|
||
{/* ── ▾ 동작 옵션 ─────────────────────────── */}
|
||
<CPGroup title="동작 옵션" defaultOpen={false}>
|
||
<FeatureChipGrid
|
||
items={[
|
||
{
|
||
key: "showHeader",
|
||
label: "헤더",
|
||
default: true,
|
||
desc: "컬럼 라벨이 있는 상단 헤더 행을 표시합니다.\n끄면 데이터 행만 보여요.",
|
||
},
|
||
{
|
||
key: "showFooter",
|
||
label: "푸터",
|
||
default: true,
|
||
desc: "테이블 하단에 페이지네이션 / 합계 등을 표시합니다.\n페이지네이션이 비활성이면 자리만 차지해요.",
|
||
},
|
||
{
|
||
key: "showCheckbox",
|
||
label: "체크박스",
|
||
desc: "각 행 좌측에 선택 체크박스 컬럼이 추가됩니다.\n[행 선택] = 복수 일 때 함께 켜는 게 일반적이에요.",
|
||
},
|
||
{
|
||
key: "striped",
|
||
label: "줄무늬",
|
||
default: true,
|
||
desc: "짝수 행에 옅은 음영을 주어 행 구분이 쉬워집니다.\n행이 많은 표 (50행+) 에 권장.",
|
||
},
|
||
{
|
||
key: "hoverable",
|
||
label: "호버 효과",
|
||
default: true,
|
||
desc: "행 위에 마우스를 올리면 배경이 밝아져 강조됩니다.\n인터랙티브 테이블 (행 클릭/선택) 에 유용.",
|
||
},
|
||
{
|
||
key: "showToolbar",
|
||
label: "툴바",
|
||
default: true,
|
||
desc: "테이블 상단에 검색 / 새로고침 / 엑셀 등 도구 모음을 표시합니다.\n별도 검색 컴포넌트가 있으면 OFF 권장.",
|
||
},
|
||
{
|
||
key: "bordered",
|
||
label: "테두리",
|
||
desc: "셀 사이에 보더 선을 추가합니다.\n많은 컬럼을 명확히 구분할 때 유용.",
|
||
},
|
||
]}
|
||
source={current as any}
|
||
onToggle={(k, v) => patch({ [k]: v } as Partial<TableConfig>)}
|
||
/>
|
||
</CPGroup>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
InvTableConfigPanel.displayName = "InvTableConfigPanel";
|
||
|
||
// ───────────────────────────────────────────────────────
|
||
// ColumnEditRow — 컬럼 한 줄 편집 (dense)
|
||
// ───────────────────────────────────────────────────────
|
||
function ColumnEditRow({
|
||
index,
|
||
col,
|
||
isLast,
|
||
onChange,
|
||
onRemove,
|
||
}: {
|
||
index: number;
|
||
col: TableColumn;
|
||
isLast: boolean;
|
||
onChange: (p: Partial<TableColumn>) => void;
|
||
onRemove: () => void;
|
||
}) {
|
||
const [hover, setHover] = useState(false);
|
||
const [expanded, setExpanded] = useState(false);
|
||
|
||
return (
|
||
<div
|
||
onMouseEnter={() => setHover(true)}
|
||
onMouseLeave={() => setHover(false)}
|
||
style={{
|
||
borderBottom: isLast ? "none" : "1px solid var(--cp-border-subtle)",
|
||
background: hover ? "var(--cp-surface-hover, var(--cp-surface))" : "transparent",
|
||
transition: "background .12s ease",
|
||
fontFamily: "var(--v5-font-sans)",
|
||
}}
|
||
>
|
||
{/* 한 줄: # + 라벨 + key + 펼침 화살표 + 삭제 */}
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "16px 1fr 1fr 18px 22px",
|
||
alignItems: "center",
|
||
columnGap: 6,
|
||
padding: "5px 8px",
|
||
minHeight: 28,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 9,
|
||
color: "var(--cp-text-muted)",
|
||
fontFamily: "var(--v5-font-mono)",
|
||
textAlign: "right",
|
||
}}
|
||
>
|
||
{index + 1}
|
||
</span>
|
||
<input
|
||
type="text"
|
||
value={col.label}
|
||
onChange={(e) => onChange({ label: e.target.value })}
|
||
placeholder="라벨"
|
||
style={inputStyle()}
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={col.key}
|
||
onChange={(e) => onChange({ key: e.target.value })}
|
||
placeholder="컬럼 key"
|
||
style={inputStyle({ mono: true })}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setExpanded((x) => !x)}
|
||
title={expanded ? "접기" : "펼침"}
|
||
style={{
|
||
width: 18,
|
||
height: 18,
|
||
padding: 0,
|
||
background: "transparent",
|
||
border: "none",
|
||
cursor: "pointer",
|
||
color: "var(--cp-text-muted)",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 10,
|
||
}}
|
||
>
|
||
{expanded ? "▾" : "▸"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onRemove}
|
||
title="제거"
|
||
style={{
|
||
width: 22,
|
||
height: 22,
|
||
padding: 0,
|
||
background: "transparent",
|
||
border: "none",
|
||
cursor: "pointer",
|
||
color: hover ? "var(--v5-red, #ef4444)" : "transparent",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
borderRadius: 4,
|
||
transition: "color .12s ease, background .12s ease",
|
||
}}
|
||
onMouseEnter={(e) =>
|
||
((e.currentTarget as HTMLButtonElement).style.background =
|
||
"rgba(239, 68, 68, 0.10)")
|
||
}
|
||
onMouseLeave={(e) =>
|
||
((e.currentTarget as HTMLButtonElement).style.background = "transparent")
|
||
}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{/* 펼친 옵션: 너비 / 정렬 / 정렬 가능 */}
|
||
{expanded && (
|
||
<div
|
||
style={{
|
||
padding: "0 8px 8px 30px",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: 6,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "1fr 1fr",
|
||
gap: 6,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<CPRow label="너비 (px)">
|
||
<CPNumber
|
||
value={col.width ?? undefined}
|
||
onChange={(v) => onChange({ width: v })}
|
||
placeholder="auto"
|
||
min={0}
|
||
/>
|
||
</CPRow>
|
||
<CPRow label="정렬">
|
||
<CPSegment
|
||
value={col.align || "left"}
|
||
onChange={(v) => onChange({ align: v as TableColumn["align"] })}
|
||
options={[
|
||
{ value: "left", label: <AlignLeft size={11} /> },
|
||
{ value: "center", label: <AlignCenter size={11} /> },
|
||
{ value: "right", label: <AlignRight size={11} /> },
|
||
]}
|
||
/>
|
||
</CPRow>
|
||
</div>
|
||
<CPRow label="정렬 가능" help="헤더 클릭 시 sort 토글">
|
||
<CPSwitch
|
||
value={col.sortable ?? true}
|
||
onChange={(v) => onChange({ sortable: v })}
|
||
/>
|
||
</CPRow>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function inputStyle({ mono = false }: { mono?: boolean } = {}): React.CSSProperties {
|
||
return {
|
||
height: 22,
|
||
padding: "0 6px",
|
||
fontSize: 11,
|
||
fontFamily: mono ? "var(--v5-font-mono)" : "var(--v5-font-sans)",
|
||
background: "var(--cp-surface)",
|
||
border: "1px solid var(--cp-border)",
|
||
borderRadius: 3,
|
||
color: "var(--cp-text)",
|
||
outline: "none",
|
||
minWidth: 0,
|
||
};
|
||
}
|
||
|
||
export default InvTableConfigPanel;
|