fix: 피벗 ConfigPanel — FeatureChipGrid 로 교체 + PivotView 빈 그리드 회피

이전 commit (3ed53a670, 57ffbcbbc) 의 두 잘못 수정.

1. ConfigPanel — CPSwitch 8개 → FeatureChipGrid 1개

   CP 시스템에 다중 boolean 토글 묶음용 FeatureChipGrid 가 이미 있는데
   (CPExtras.tsx:208, InvLegacyDivider/Button/Text/InvRepeater 가 사용),
   CPRow + CPSwitch 8개로 따로 만든 게 잘못. cp 시스템 본래 패턴 따름.

   - 8 토글 (chartEnabled / fieldChooserEnabled / rowGrandTotals /
     columnGrandTotals / mergeCells / alternateRowColors / exportExcel /
     exportPdf) 을 평면 key 로 정의
   - source = nested config 에서 평면 boolean 객체 변환
   - onToggle = 평면 key 받아 nested patch (switch 분기)
   - 각 chip 에 desc 추가 (hover tooltip, FeatureChipGrid 가 portal 로 표시)

2. PivotView — data 영역 0개면 안내 (빈 0 그리드 회피)

   hasActiveFields 분기를 강화. 기존: row/column/data 중 하나만 있어도
   true → row 영역에만 컬럼이 들어간 옛 잘못된 매핑이 빈 0 그리드를 표시
   하는 회귀 (Image #6).

   변경: data 영역 컬럼 ≥1 이어야 의미있는 피벗. data 0개면 "필드를
   배치하세요" 안내 + FieldChooser 버튼 fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DDD1542
2026-04-29 16:54:30 +09:00
parent 57ffbcbbc7
commit 2d11d222db
2 changed files with 78 additions and 117 deletions
@@ -282,121 +282,82 @@ export const InvTableConfigPanel: React.FC<InvTableConfigPanelProps> = ({
{displayMode === "pivot" && ( {displayMode === "pivot" && (
<CPSection title="④ 피벗 설정" desc="본체 분석 UI에서 사용할 표시 옵션"> <CPSection title="④ 피벗 설정" desc="본체 분석 UI에서 사용할 표시 옵션">
{/* 피벗 필드 배치는 PivotView 본체의 FieldPanel/FieldChooser가 담당한다. ConfigPanel은 메타 옵션만 관리한다. */} {/* 피벗 필드 배치는 PivotView 본체의 FieldPanel/FieldChooser가 담당한다. ConfigPanel은 메타 토글만 관리한다. */}
<CPRow label="차트 표시"> <FeatureChipGrid
<CPSwitch items={[
value={current.pivotChart?.enabled ?? false} { key: "chartEnabled", label: "차트 표시", desc: "본체 안에 피벗 차트(bar/line/pie 등) 패널을 함께 표시합니다." },
onChange={(v) => { key: "fieldChooserEnabled", label: "필드 선택기", desc: "본체에서 row/column/data/filter 영역에 컬럼을 드래그-앤-드롭 배치하는 모달을 활성화." },
patch({ { key: "rowGrandTotals", label: "행 총계", desc: "각 행의 합계 행을 자동 표시합니다. row 영역 컬럼이 있을 때 의미가 있습니다." },
pivotChart: { { key: "columnGrandTotals", label: "열 총계", desc: "각 열의 합계 열을 자동 표시합니다. column 영역 컬럼이 있을 때 의미가 있습니다." },
type: current.pivotChart?.type ?? "bar", { key: "mergeCells", label: "셀 병합", desc: "같은 값의 인접 셀을 병합해 가독성을 높입니다." },
position: current.pivotChart?.position ?? "bottom", { key: "alternateRowColors", label: "행 교대 색", desc: "홀수/짝수 행을 다른 톤으로 교대 표시 (Zebra)." },
...current.pivotChart, { key: "exportExcel", label: "엑셀 내보내기", desc: "본체 툴바에 .xlsx 다운로드 버튼을 활성화." },
enabled: v, { key: "exportPdf", label: "PDF 내보내기", desc: "본체 툴바에 PDF 다운로드 버튼을 활성화." },
}, ]}
}) source={{
} chartEnabled: current.pivotChart?.enabled ?? false,
/> fieldChooserEnabled: current.pivotFieldChooser?.enabled ?? false,
</CPRow> rowGrandTotals: current.pivotTotals?.showRowGrandTotals ?? false,
<CPRow label="필드 선택기"> columnGrandTotals: current.pivotTotals?.showColumnGrandTotals ?? false,
<CPSwitch mergeCells: current.pivotStyle?.mergeCells ?? false,
value={current.pivotFieldChooser?.enabled ?? false} alternateRowColors: current.pivotStyle?.alternateRowColors ?? false,
onChange={(v) => exportExcel: current.pivotExportConfig?.excel ?? false,
patch({ exportPdf: current.pivotExportConfig?.pdf ?? false,
pivotFieldChooser: { }}
...current.pivotFieldChooser, onToggle={(key, value) => {
enabled: v, switch (key) {
}, case "chartEnabled":
}) patch({
} pivotChart: {
/> type: current.pivotChart?.type ?? "bar",
</CPRow> position: current.pivotChart?.position ?? "bottom",
<CPRow label="행 총계"> ...current.pivotChart,
<CPSwitch enabled: value,
value={current.pivotTotals?.showRowGrandTotals ?? false} },
onChange={(v) => });
patch({ return;
pivotTotals: { case "fieldChooserEnabled":
...current.pivotTotals, patch({
showRowGrandTotals: v, pivotFieldChooser: { ...current.pivotFieldChooser, enabled: value },
}, });
}) return;
} case "rowGrandTotals":
/> patch({
</CPRow> pivotTotals: { ...current.pivotTotals, showRowGrandTotals: value },
<CPRow label="열 총계"> });
<CPSwitch return;
value={current.pivotTotals?.showColumnGrandTotals ?? false} case "columnGrandTotals":
onChange={(v) => patch({
patch({ pivotTotals: { ...current.pivotTotals, showColumnGrandTotals: value },
pivotTotals: { });
...current.pivotTotals, return;
showColumnGrandTotals: v, case "mergeCells":
}, case "alternateRowColors": {
}) const baseStyle = {
} theme: current.pivotStyle?.theme ?? "default" as const,
/> headerStyle: current.pivotStyle?.headerStyle ?? "default" as const,
</CPRow> cellPadding: current.pivotStyle?.cellPadding ?? "normal" as const,
<CPRow label="셀 병합"> borderStyle: current.pivotStyle?.borderStyle ?? "light" as const,
<CPSwitch
value={current.pivotStyle?.mergeCells ?? false}
onChange={(v) =>
patch({
pivotStyle: {
theme: current.pivotStyle?.theme ?? "default",
headerStyle: current.pivotStyle?.headerStyle ?? "default",
cellPadding: current.pivotStyle?.cellPadding ?? "normal",
borderStyle: current.pivotStyle?.borderStyle ?? "light",
...current.pivotStyle, ...current.pivotStyle,
mergeCells: v, };
}, patch({
}) pivotStyle: { ...baseStyle, [key]: value },
});
return;
}
case "exportExcel":
patch({
pivotExportConfig: { ...current.pivotExportConfig, excel: value },
});
return;
case "exportPdf":
patch({
pivotExportConfig: { ...current.pivotExportConfig, pdf: value },
});
return;
} }
/> }}
</CPRow> />
<CPRow label="행 교대 색">
<CPSwitch
value={current.pivotStyle?.alternateRowColors ?? false}
onChange={(v) =>
patch({
pivotStyle: {
theme: current.pivotStyle?.theme ?? "default",
headerStyle: current.pivotStyle?.headerStyle ?? "default",
cellPadding: current.pivotStyle?.cellPadding ?? "normal",
borderStyle: current.pivotStyle?.borderStyle ?? "light",
...current.pivotStyle,
alternateRowColors: v,
},
})
}
/>
</CPRow>
<CPRow label="엑셀 내보내기">
<CPSwitch
value={current.pivotExportConfig?.excel ?? false}
onChange={(v) =>
patch({
pivotExportConfig: {
...current.pivotExportConfig,
excel: v,
},
})
}
/>
</CPRow>
<CPRow label="PDF 내보내기">
<CPSwitch
value={current.pivotExportConfig?.pdf ?? false}
onChange={(v) =>
patch({
pivotExportConfig: {
...current.pivotExportConfig,
pdf: v,
},
})
}
/>
</CPRow>
</CPSection> </CPSection>
)} )}
@@ -1045,10 +1045,10 @@ export const PivotView: React.FC<PivotGridProps> = ({
); );
} }
// 필드 미설정 (행, 열, 데이터 영역에 필드가 있는지 확인) // 필드 미설정 — data 영역 (집계 대상) 이 1개 이상 있어야 의미있는 피벗.
const hasActiveFields = fields.some( // row/column 만 있고 data 가 비어있으면 빈 0 그리드가 생성되므로 안내로 fallback.
(f) => f.visible !== false && ["row", "column", "data"].includes(f.area) const hasDataField = fields.some((f) => f.visible !== false && f.area === "data");
); const hasActiveFields = hasDataField;
if (!hasActiveFields) { if (!hasActiveFields) {
return ( return (
<div <div