fix(pop-dashboard): 차트 X/Y축 자동 적용 및 데이터 처리 안정화
설정 패널 간소화: - 차트 X축/Y축 수동 입력 필드 제거 (자동 적용 안내 문구로 대체) - groupBy 선택 시 X축 자동, 집계 결과를 Y축(value)으로 자동 매핑 차트 렌더링 개선 (ChartItem): - PieChart에 카테고리명+값+비율 라벨 표시 - Legend 컴포넌트 추가 (containerWidth 300px 이상 시) - Tooltip formatter로 이름/값 쌍 표시 데이터 fetcher 안정화 (dataFetcher): - apiClient(axios) 우선 호출, dashboardApi(fetch) 폴백 패턴 적용 - PostgreSQL bigint/numeric 문자열 -> 숫자 자동 변환 처리 - Recharts가 숫자 타입을 요구하는 문제 해결 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
578cca2687
commit
7a71fc6ca7
|
|
@ -1289,52 +1289,11 @@ function ItemEditor({
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* X축 컬럼 */}
|
||||
<div>
|
||||
<Label className="text-xs">X축 컬럼</Label>
|
||||
<Input
|
||||
value={item.chartConfig?.xAxisColumn ?? ""}
|
||||
onChange={(e) =>
|
||||
onUpdate({
|
||||
...item,
|
||||
chartConfig: {
|
||||
...item.chartConfig,
|
||||
chartType: item.chartConfig?.chartType ?? "bar",
|
||||
xAxisColumn: e.target.value || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="groupBy 컬럼명 (비우면 자동)"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
그룹핑 컬럼명과 동일하게 입력. 비우면 첫 번째 groupBy 컬럼 사용
|
||||
{/* X축/Y축 자동 안내 */}
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
X축: 그룹핑(X축)에서 선택한 컬럼 자동 적용 / Y축: 집계 결과(value) 자동 적용
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Y축 컬럼 */}
|
||||
<div>
|
||||
<Label className="text-xs">Y축 컬럼</Label>
|
||||
<Input
|
||||
value={item.chartConfig?.yAxisColumn ?? ""}
|
||||
onChange={(e) =>
|
||||
onUpdate({
|
||||
...item,
|
||||
chartConfig: {
|
||||
...item.chartConfig,
|
||||
chartType: item.chartConfig?.chartType ?? "bar",
|
||||
yAxisColumn: e.target.value || undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="집계 결과 컬럼명 (비우면 자동)"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
집계 결과 컬럼. 비우면 집계 함수 결과를 자동 사용
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.subType === "gauge" && (
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import type { DashboardItem } from "../../types";
|
||||
|
|
@ -124,7 +125,7 @@ export function ChartItemComponent({
|
|||
/>
|
||||
</LineChart>
|
||||
) : (
|
||||
/* pie */
|
||||
/* pie - 카테고리명 + 값 라벨 표시 */
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={rows as Record<string, string | number>[]}
|
||||
|
|
@ -132,8 +133,14 @@ export function ChartItemComponent({
|
|||
nameKey={xKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius="80%"
|
||||
label={containerWidth > 250}
|
||||
outerRadius={containerWidth > 400 ? "70%" : "80%"}
|
||||
label={
|
||||
containerWidth > 250
|
||||
? ({ name, value, percent }: { name: string; value: number; percent: number }) =>
|
||||
`${name} ${value} (${(percent * 100).toFixed(0)}%)`
|
||||
: false
|
||||
}
|
||||
labelLine={containerWidth > 250}
|
||||
>
|
||||
{rows.map((_, index) => (
|
||||
<Cell
|
||||
|
|
@ -142,7 +149,15 @@ export function ChartItemComponent({
|
|||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => [value, name]}
|
||||
/>
|
||||
{containerWidth > 300 && (
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: 11 }}
|
||||
iconSize={10}
|
||||
/>
|
||||
)}
|
||||
</PieChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
* - fetch 직접 사용 금지: 반드시 dashboardApi/dataApi 사용
|
||||
*/
|
||||
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { dashboardApi } from "@/lib/api/dashboard";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
|
|
@ -238,19 +239,46 @@ export async function fetchAggregatedData(
|
|||
// 집계 또는 조인이 있으면 SQL 직접 실행
|
||||
if (config.aggregation || (config.joins && config.joins.length > 0)) {
|
||||
const sql = buildAggregationSQL(config);
|
||||
const result = await dashboardApi.executeQuery(sql);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
// API 호출: apiClient(axios) 우선, dashboardApi(fetch) 폴백
|
||||
let queryResult: { columns: string[]; rows: any[] };
|
||||
try {
|
||||
// 1차: apiClient (axios 기반, 인증/세션 안정적)
|
||||
const response = await apiClient.post("/dashboards/execute-query", { query: sql });
|
||||
if (response.data?.success && response.data?.data) {
|
||||
queryResult = response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data?.message || "쿼리 실행 실패");
|
||||
}
|
||||
} catch {
|
||||
// 2차: dashboardApi (fetch 기반, 폴백)
|
||||
queryResult = await dashboardApi.executeQuery(sql);
|
||||
}
|
||||
|
||||
if (queryResult.rows.length === 0) {
|
||||
return { value: 0, rows: [] };
|
||||
}
|
||||
|
||||
// PostgreSQL bigint/numeric는 JS에서 문자열로 반환됨
|
||||
// Recharts PieChart 등은 숫자 타입이 필수이므로 변환 처리
|
||||
const processedRows = queryResult.rows.map((row: Record<string, unknown>) => {
|
||||
const converted: Record<string, unknown> = { ...row };
|
||||
for (const key of Object.keys(converted)) {
|
||||
const val = converted[key];
|
||||
if (typeof val === "string" && val !== "" && !isNaN(Number(val))) {
|
||||
converted[key] = Number(val);
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
});
|
||||
|
||||
// 첫 번째 행의 value 컬럼 추출
|
||||
const firstRow = result.rows[0];
|
||||
const numericValue = parseFloat(firstRow.value ?? firstRow[result.columns[0]] ?? 0);
|
||||
const firstRow = processedRows[0];
|
||||
const numericValue = parseFloat(String(firstRow.value ?? firstRow[queryResult.columns[0]] ?? 0));
|
||||
|
||||
return {
|
||||
value: Number.isFinite(numericValue) ? numericValue : 0,
|
||||
rows: result.rows,
|
||||
rows: processedRows,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue