feat: InvTableConfigPanel pivot/card 옵션 UI (T4)

displayMode 분기 옵션 채움. T3b 에서 흡수한 PivotView + CardView 가
실제로 동작하도록 ConfigPanel UI 노출. cp 프리미티브만 사용.

pivot 분기 (placeholder 제거)
- columns 별 영역 매핑 (none/row/column/data/filter) — CPSelect
- data 영역인 경우 집계 함수 (sum/count/avg/min/max/countDistinct) — CPSelect
- pivotFields[] 배열로 자동 변환 (area="none" 선택 시 항목 제거)
- 컬럼 미로드 시 안내 Hint

card 분기 (신규)
- 그리드: cardsPerRow (1~10), cardSpacing (0~64px) — CPNumber
- 표시 영역 (CPGroup defaultOpen): 제목/부제/설명/이미지 표시 토글 +
  이미지 위치 (top/left/right) + 이미지 크기 (small/medium/large)
- 컬럼 매핑 (CPGroup defaultOpen): titleColumn/subtitleColumn/
  descriptionColumn/imageColumn — CPSelect from columns
- 액션 버튼 (CPGroup defaultOpen=false): showActions 토글 + showView/Edit/
  DeleteButton 조건부 노출

cp-panel-standard 룰 준수 (CPSection > CPGroup > CPRow > CPSelect/Switch
/Segment/Number, 3-depth 미만, 카드형 외곽 X).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DDD1542
2026-04-29 14:31:45 +09:00
parent 49b4cdf562
commit 3e6bce70d1
@@ -281,12 +281,273 @@ export const InvTableConfigPanel: React.FC<InvTableConfigPanelProps> = ({
)}
{displayMode === "pivot" && (
<CPSection title="④ 피벗 설정" desc="개발 중">
<Hint tone="warn">
row/column/values UI는 .
<br />
displayMode ( ).
</Hint>
<CPSection title="④ 피벗 설정" desc="row · column · data 영역에 컬럼 배치">
{columns.length === 0 ? (
<Hint tone="warn"> .</Hint>
) : (
<>
<Hint>
. data .
</Hint>
{columns.map((col, idx) => {
const fields = current.pivotFields ?? [];
const fieldIdx = fields.findIndex((f) => f.field === col.key);
const field = fieldIdx >= 0 ? fields[fieldIdx] : undefined;
const area = field?.area ?? "none";
const summaryType = field?.summaryType ?? "sum";
const updateField = (next: Partial<NonNullable<TableConfig["pivotFields"]>[number]> | "remove") => {
const list = [...fields];
if (next === "remove") {
if (fieldIdx >= 0) list.splice(fieldIdx, 1);
} else if (fieldIdx >= 0) {
list[fieldIdx] = { ...list[fieldIdx], ...next };
} else {
list.push({
field: col.key,
caption: col.label || col.key,
area: "row",
...next,
});
}
patch({ pivotFields: list });
};
return (
<CPRow key={col.key} label={col.label || col.key}>
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
<CPSelect
value={area}
onChange={(v) => {
if (v === "none") updateField("remove");
else updateField({ area: v as any });
}}
searchable={false}
>
<option value="none"></option>
<option value="row">row</option>
<option value="column">column</option>
<option value="data">data</option>
<option value="filter">filter</option>
</CPSelect>
{area === "data" && (
<CPSelect
value={summaryType}
onChange={(v) => updateField({ summaryType: v as any })}
searchable={false}
>
<option value="sum"></option>
<option value="count"></option>
<option value="avg"></option>
<option value="min"></option>
<option value="max"></option>
<option value="countDistinct"> </option>
</CPSelect>
)}
</div>
</CPRow>
);
})}
</>
)}
</CPSection>
)}
{displayMode === "card" && (
<CPSection title="④ 카드 설정" desc="grid · 표시 영역 · 컬럼 매핑">
<CPRow label="한 줄 카드 수">
<CPNumber
value={current.cardsPerRow ?? 3}
onChange={(v) => patch({ cardsPerRow: v ?? 3 })}
min={1}
max={10}
/>
</CPRow>
<CPRow label="간격(px)">
<CPNumber
value={current.cardSpacing ?? 12}
onChange={(v) => patch({ cardSpacing: v ?? 12 })}
min={0}
max={64}
/>
</CPRow>
<CPGroup title="표시 영역" defaultOpen>
<CPRow label="제목">
<CPSwitch
value={current.cardStyle?.showTitle ?? true}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showTitle: v } })}
/>
</CPRow>
<CPRow label="부제">
<CPSwitch
value={current.cardStyle?.showSubtitle ?? true}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showSubtitle: v } })}
/>
</CPRow>
<CPRow label="설명">
<CPSwitch
value={current.cardStyle?.showDescription ?? true}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showDescription: v } })}
/>
</CPRow>
<CPRow label="이미지">
<CPSwitch
value={current.cardStyle?.showImage ?? true}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showImage: v } })}
/>
</CPRow>
{(current.cardStyle?.showImage ?? true) && (
<>
<CPRow label="이미지 위치">
<CPSegment
value={current.cardStyle?.imagePosition || "top"}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, imagePosition: v as any } })}
options={[
{ value: "top", label: "상단" },
{ value: "left", label: "좌측" },
{ value: "right", label: "우측" },
]}
/>
</CPRow>
<CPRow label="이미지 크기">
<CPSegment
value={current.cardStyle?.imageSize || "medium"}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, imageSize: v as any } })}
options={[
{ value: "small", label: "작게" },
{ value: "medium", label: "보통" },
{ value: "large", label: "크게" },
]}
/>
</CPRow>
</>
)}
</CPGroup>
<CPGroup title="컬럼 매핑" defaultOpen>
{columns.length === 0 ? (
<Hint tone="warn"> .</Hint>
) : (
<>
<CPRow label="제목 컬럼">
<CPSelect
value={current.cardColumnMapping?.titleColumn || ""}
onChange={(v) =>
patch({
cardColumnMapping: {
...current.cardColumnMapping,
titleColumn: v || undefined,
},
})
}
>
<option value="">...</option>
{columns.map((c) => (
<option key={c.key} value={c.key}>
{c.label || c.key}
</option>
))}
</CPSelect>
</CPRow>
<CPRow label="부제 컬럼">
<CPSelect
value={current.cardColumnMapping?.subtitleColumn || ""}
onChange={(v) =>
patch({
cardColumnMapping: {
...current.cardColumnMapping,
subtitleColumn: v || undefined,
},
})
}
>
<option value="">...</option>
{columns.map((c) => (
<option key={c.key} value={c.key}>
{c.label || c.key}
</option>
))}
</CPSelect>
</CPRow>
<CPRow label="설명 컬럼">
<CPSelect
value={current.cardColumnMapping?.descriptionColumn || ""}
onChange={(v) =>
patch({
cardColumnMapping: {
...current.cardColumnMapping,
descriptionColumn: v || undefined,
},
})
}
>
<option value="">...</option>
{columns.map((c) => (
<option key={c.key} value={c.key}>
{c.label || c.key}
</option>
))}
</CPSelect>
</CPRow>
<CPRow label="이미지 컬럼">
<CPSelect
value={current.cardColumnMapping?.imageColumn || ""}
onChange={(v) =>
patch({
cardColumnMapping: {
...current.cardColumnMapping,
imageColumn: v || undefined,
},
})
}
>
<option value="">...</option>
{columns.map((c) => (
<option key={c.key} value={c.key}>
{c.label || c.key}
</option>
))}
</CPSelect>
</CPRow>
</>
)}
</CPGroup>
<CPGroup title="액션 버튼" defaultOpen={false}>
<CPRow label="액션 표시">
<CPSwitch
value={current.cardStyle?.showActions ?? false}
onChange={(v) => patch({ cardStyle: { ...current.cardStyle, showActions: v } })}
/>
</CPRow>
{current.cardStyle?.showActions && (
<>
<CPRow label="보기">
<CPSwitch
value={current.cardStyle?.showViewButton ?? false}
onChange={(v) =>
patch({ cardStyle: { ...current.cardStyle, showViewButton: v } })
}
/>
</CPRow>
<CPRow label="편집">
<CPSwitch
value={current.cardStyle?.showEditButton ?? false}
onChange={(v) =>
patch({ cardStyle: { ...current.cardStyle, showEditButton: v } })
}
/>
</CPRow>
<CPRow label="삭제">
<CPSwitch
value={current.cardStyle?.showDeleteButton ?? false}
onChange={(v) =>
patch({ cardStyle: { ...current.cardStyle, showDeleteButton: v } })
}
/>
</CPRow>
</>
)}
</CPGroup>
</CPSection>
)}