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:
SeongHyun Kim
2026-02-10 16:55:34 +09:00
parent 578cca2687
commit 7a71fc6ca7
3 changed files with 56 additions and 54 deletions

View File

@@ -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,
};
}