feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
/**
|
|
|
|
|
* pop-dashboard 데이터 페처
|
|
|
|
|
*
|
|
|
|
|
* @INFRA-EXTRACT: useDataSource 완성 후 이 파일 전체를 훅으로 교체 예정
|
|
|
|
|
* 현재는 직접 API 호출. 공통 인프라 완성 후 useDataSource 훅으로 교체.
|
|
|
|
|
*
|
|
|
|
|
* 보안:
|
|
|
|
|
* - SQL 인젝션 방지: 사용자 입력값 이스케이프 처리
|
|
|
|
|
* - 멀티테넌시: autoFilter 자동 전달
|
|
|
|
|
* - fetch 직접 사용 금지: 반드시 dashboardApi/dataApi 사용
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-10 16:55:34 +09:00
|
|
|
import { apiClient } from "@/lib/api/client";
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
import { dashboardApi } from "@/lib/api/dashboard";
|
|
|
|
|
import { dataApi } from "@/lib/api/data";
|
2026-02-10 14:22:30 +09:00
|
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
|
|
|
import type { TableInfo } from "@/lib/api/tableManagement";
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
import type { DataSourceConfig, DataSourceFilter } from "../../types";
|
|
|
|
|
|
2026-02-10 14:22:30 +09:00
|
|
|
// ===== 타입 re-export =====
|
|
|
|
|
|
|
|
|
|
export type { TableInfo };
|
|
|
|
|
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
// ===== 반환 타입 =====
|
|
|
|
|
|
|
|
|
|
export interface AggregatedResult {
|
|
|
|
|
value: number;
|
|
|
|
|
rows?: Record<string, unknown>[];
|
|
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ColumnInfo {
|
|
|
|
|
name: string;
|
|
|
|
|
type: string;
|
|
|
|
|
udtName: string;
|
2026-03-03 15:30:07 +09:00
|
|
|
isPrimaryKey?: boolean;
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== SQL 값 이스케이프 =====
|
|
|
|
|
|
|
|
|
|
/** SQL 문자열 값 이스케이프 (SQL 인젝션 방지) */
|
|
|
|
|
function escapeSQL(value: unknown): string {
|
|
|
|
|
if (value === null || value === undefined) return "NULL";
|
|
|
|
|
if (typeof value === "number") return String(value);
|
|
|
|
|
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
|
|
|
// 문자열: 작은따옴표 이스케이프
|
|
|
|
|
const str = String(value).replace(/'/g, "''");
|
|
|
|
|
return `'${str}'`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 16:12:29 +09:00
|
|
|
// ===== 설정 완료 여부 검증 =====
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DataSourceConfig의 필수값이 모두 채워졌는지 검증
|
|
|
|
|
* 설정 중간 상태(테이블 미선택, 컬럼 미선택 등)에서는
|
|
|
|
|
* SQL을 생성하지 않도록 사전 차단
|
|
|
|
|
*
|
|
|
|
|
* @returns null이면 유효, 문자열이면 미완료 사유
|
|
|
|
|
*/
|
|
|
|
|
function validateDataSourceConfig(config: DataSourceConfig): string | null {
|
|
|
|
|
// 테이블명 필수
|
|
|
|
|
if (!config.tableName || !config.tableName.trim()) {
|
|
|
|
|
return "테이블이 선택되지 않았습니다";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 집계 함수가 설정되었으면 대상 컬럼도 필수
|
|
|
|
|
// (단, COUNT는 컬럼 없이도 COUNT(*)로 처리 가능)
|
|
|
|
|
if (config.aggregation) {
|
|
|
|
|
const aggType = config.aggregation.type?.toLowerCase();
|
|
|
|
|
const aggCol = config.aggregation.column?.trim();
|
|
|
|
|
if (aggType !== "count" && !aggCol) {
|
|
|
|
|
return `${config.aggregation.type.toUpperCase()} 집계에 대상 컬럼이 필요합니다`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 조인이 있으면 조인 조건 필수
|
|
|
|
|
if (config.joins?.length) {
|
|
|
|
|
for (const join of config.joins) {
|
|
|
|
|
if (!join.targetTable?.trim()) {
|
|
|
|
|
return "조인 대상 테이블이 선택되지 않았습니다";
|
|
|
|
|
}
|
|
|
|
|
if (!join.on.sourceColumn?.trim() || !join.on.targetColumn?.trim()) {
|
|
|
|
|
return "조인 조건 컬럼이 설정되지 않았습니다";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
// ===== 필터 조건 SQL 생성 =====
|
|
|
|
|
|
|
|
|
|
/** DataSourceFilter 배열을 WHERE 절 조건문으로 변환 */
|
|
|
|
|
function buildWhereClause(filters: DataSourceFilter[]): string {
|
2026-02-10 16:12:29 +09:00
|
|
|
// 컬럼명이 빈 필터는 무시 (설정 중간 상태 방어)
|
|
|
|
|
const validFilters = filters.filter((f) => f.column?.trim());
|
|
|
|
|
if (!validFilters.length) return "";
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
|
2026-02-10 16:12:29 +09:00
|
|
|
const conditions = validFilters.map((f) => {
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
const col = sanitizeIdentifier(f.column);
|
|
|
|
|
|
|
|
|
|
switch (f.operator) {
|
|
|
|
|
case "between": {
|
|
|
|
|
const arr = Array.isArray(f.value) ? f.value : [f.value, f.value];
|
|
|
|
|
return `${col} BETWEEN ${escapeSQL(arr[0])} AND ${escapeSQL(arr[1])}`;
|
|
|
|
|
}
|
|
|
|
|
case "in": {
|
|
|
|
|
const arr = Array.isArray(f.value) ? f.value : [f.value];
|
|
|
|
|
const vals = arr.map(escapeSQL).join(", ");
|
|
|
|
|
return `${col} IN (${vals})`;
|
|
|
|
|
}
|
|
|
|
|
case "like":
|
|
|
|
|
return `${col} LIKE ${escapeSQL(f.value)}`;
|
|
|
|
|
default:
|
|
|
|
|
return `${col} ${f.operator} ${escapeSQL(f.value)}`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return `WHERE ${conditions.join(" AND ")}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 식별자 검증 (테이블명, 컬럼명) =====
|
|
|
|
|
|
|
|
|
|
/** SQL 식별자(테이블명, 컬럼명)에서 위험한 문자 제거 */
|
|
|
|
|
function sanitizeIdentifier(name: string): string {
|
|
|
|
|
// 알파벳, 숫자, 언더스코어, 점(스키마.테이블)만 허용
|
|
|
|
|
return name.replace(/[^a-zA-Z0-9_.]/g, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 집계 SQL 빌더 =====
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DataSourceConfig를 SELECT SQL로 변환
|
|
|
|
|
*
|
|
|
|
|
* @param config - 데이터 소스 설정
|
|
|
|
|
* @returns SQL 문자열
|
|
|
|
|
*/
|
|
|
|
|
export function buildAggregationSQL(config: DataSourceConfig): string {
|
|
|
|
|
const tableName = sanitizeIdentifier(config.tableName);
|
|
|
|
|
|
|
|
|
|
// SELECT 절
|
|
|
|
|
let selectClause: string;
|
|
|
|
|
if (config.aggregation) {
|
|
|
|
|
const aggType = config.aggregation.type.toUpperCase();
|
2026-02-10 16:12:29 +09:00
|
|
|
const aggCol = config.aggregation.column?.trim()
|
|
|
|
|
? sanitizeIdentifier(config.aggregation.column)
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
// COUNT는 컬럼 없으면 COUNT(*), 나머지는 컬럼 필수
|
|
|
|
|
if (!aggCol) {
|
|
|
|
|
selectClause = aggType === "COUNT"
|
|
|
|
|
? "COUNT(*) as value"
|
|
|
|
|
: `${aggType}(${tableName}.*) as value`;
|
|
|
|
|
} else {
|
|
|
|
|
selectClause = `${aggType}(${aggCol}) as value`;
|
|
|
|
|
}
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
|
|
|
|
|
// GROUP BY가 있으면 해당 컬럼도 SELECT에 포함
|
|
|
|
|
if (config.aggregation.groupBy?.length) {
|
|
|
|
|
const groupCols = config.aggregation.groupBy.map(sanitizeIdentifier).join(", ");
|
|
|
|
|
selectClause = `${groupCols}, ${selectClause}`;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
selectClause = "*";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 16:12:29 +09:00
|
|
|
// FROM 절 (조인 포함 - 조건이 완전한 조인만 적용)
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
let fromClause = tableName;
|
|
|
|
|
if (config.joins?.length) {
|
|
|
|
|
for (const join of config.joins) {
|
2026-02-10 16:12:29 +09:00
|
|
|
// 조인 설정이 불완전하면 건너뜀 (설정 중간 상태 방어)
|
|
|
|
|
if (!join.targetTable?.trim() || !join.on.sourceColumn?.trim() || !join.on.targetColumn?.trim()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
const joinTable = sanitizeIdentifier(join.targetTable);
|
|
|
|
|
const joinType = join.joinType.toUpperCase();
|
|
|
|
|
const srcCol = sanitizeIdentifier(join.on.sourceColumn);
|
|
|
|
|
const tgtCol = sanitizeIdentifier(join.on.targetColumn);
|
|
|
|
|
fromClause += ` ${joinType} JOIN ${joinTable} ON ${tableName}.${srcCol} = ${joinTable}.${tgtCol}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WHERE 절
|
|
|
|
|
const whereClause = config.filters?.length
|
|
|
|
|
? buildWhereClause(config.filters)
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
// GROUP BY 절
|
|
|
|
|
let groupByClause = "";
|
|
|
|
|
if (config.aggregation?.groupBy?.length) {
|
|
|
|
|
groupByClause = `GROUP BY ${config.aggregation.groupBy.map(sanitizeIdentifier).join(", ")}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ORDER BY 절
|
|
|
|
|
let orderByClause = "";
|
|
|
|
|
if (config.sort?.length) {
|
|
|
|
|
const sortCols = config.sort
|
|
|
|
|
.map((s) => `${sanitizeIdentifier(s.column)} ${s.direction.toUpperCase()}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
orderByClause = `ORDER BY ${sortCols}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LIMIT 절
|
|
|
|
|
const limitClause = config.limit ? `LIMIT ${Math.max(1, Math.floor(config.limit))}` : "";
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
`SELECT ${selectClause}`,
|
|
|
|
|
`FROM ${fromClause}`,
|
|
|
|
|
whereClause,
|
|
|
|
|
groupByClause,
|
|
|
|
|
orderByClause,
|
|
|
|
|
limitClause,
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 메인 데이터 페처 =====
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DataSourceConfig 기반으로 데이터를 조회하여 집계 결과 반환
|
|
|
|
|
*
|
|
|
|
|
* API 선택 전략:
|
|
|
|
|
* 1. 집계 있음 -> buildAggregationSQL -> dashboardApi.executeQuery()
|
|
|
|
|
* 2. 조인만 있음 (2개 테이블) -> dataApi.getJoinedData() (추후 지원)
|
|
|
|
|
* 3. 단순 조회 -> dataApi.getTableData()
|
|
|
|
|
*
|
|
|
|
|
* @INFRA-EXTRACT: useDataSource 완성 후 이 함수를 훅으로 교체
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchAggregatedData(
|
|
|
|
|
config: DataSourceConfig
|
|
|
|
|
): Promise<AggregatedResult> {
|
|
|
|
|
try {
|
2026-02-10 16:12:29 +09:00
|
|
|
// 설정 완료 여부 검증 (미완료 시 SQL 전송 차단)
|
|
|
|
|
const validationError = validateDataSourceConfig(config);
|
|
|
|
|
if (validationError) {
|
|
|
|
|
return { value: 0, rows: [], error: validationError };
|
|
|
|
|
}
|
|
|
|
|
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
// 집계 또는 조인이 있으면 SQL 직접 실행
|
|
|
|
|
if (config.aggregation || (config.joins && config.joins.length > 0)) {
|
|
|
|
|
const sql = buildAggregationSQL(config);
|
|
|
|
|
|
2026-02-10 16:55:34 +09:00
|
|
|
// 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) {
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
return { value: 0, rows: [] };
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 16:55:34 +09:00
|
|
|
// 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;
|
|
|
|
|
});
|
|
|
|
|
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
// 첫 번째 행의 value 컬럼 추출
|
2026-02-10 16:55:34 +09:00
|
|
|
const firstRow = processedRows[0];
|
|
|
|
|
const numericValue = parseFloat(String(firstRow.value ?? firstRow[queryResult.columns[0]] ?? 0));
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value: Number.isFinite(numericValue) ? numericValue : 0,
|
2026-02-10 16:55:34 +09:00
|
|
|
rows: processedRows,
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 단순 조회
|
|
|
|
|
const tableResult = await dataApi.getTableData(config.tableName, {
|
|
|
|
|
page: 1,
|
|
|
|
|
size: config.limit ?? 100,
|
|
|
|
|
sortBy: config.sort?.[0]?.column,
|
|
|
|
|
sortOrder: config.sort?.[0]?.direction,
|
|
|
|
|
filters: config.filters?.reduce(
|
|
|
|
|
(acc, f) => {
|
|
|
|
|
acc[f.column] = f.value;
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{} as Record<string, unknown>
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 단순 조회 시에는 행 수를 value로 사용
|
|
|
|
|
return {
|
|
|
|
|
value: tableResult.total ?? tableResult.data.length,
|
|
|
|
|
rows: tableResult.data,
|
|
|
|
|
};
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
const message = err instanceof Error ? err.message : "데이터 조회 실패";
|
|
|
|
|
return { value: 0, error: message };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 설정 패널용 헬퍼 =====
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 목록 조회 (설정 패널 드롭다운용)
|
|
|
|
|
* dashboardApi.getTableSchema()로 특정 테이블의 스키마를 가져오되,
|
|
|
|
|
* 테이블 목록은 별도로 필요하므로 간단히 반환
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchTableColumns(
|
|
|
|
|
tableName: string
|
|
|
|
|
): Promise<ColumnInfo[]> {
|
2026-02-10 16:12:29 +09:00
|
|
|
// 1차: tableManagementApi (apiClient/axios 기반, 인증 안정적)
|
|
|
|
|
try {
|
|
|
|
|
const response = await tableManagementApi.getTableSchema(tableName);
|
|
|
|
|
if (response.success && response.data) {
|
|
|
|
|
const cols = Array.isArray(response.data) ? response.data : [];
|
|
|
|
|
if (cols.length > 0) {
|
|
|
|
|
return cols.map((col: any) => ({
|
|
|
|
|
name: col.columnName || col.column_name || col.name,
|
|
|
|
|
type: col.dataType || col.data_type || col.type || "unknown",
|
|
|
|
|
udtName: col.dbType || col.udt_name || col.udtName || "unknown",
|
2026-03-03 15:30:07 +09:00
|
|
|
isPrimaryKey: col.isPrimaryKey === true || col.isPrimaryKey === "true" || col.is_primary_key === true || col.is_primary_key === "true",
|
2026-02-10 16:12:29 +09:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// tableManagementApi 실패 시 dashboardApi로 폴백
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2차: dashboardApi (fetch 기반, 폴백)
|
feat(pop-dashboard): Phase 0 공통 타입 + Phase 1 대시보드 컴포넌트 구현
Phase 0: 공통 인프라 타입 정의
- ColumnBinding, JoinConfig, DataSourceConfig, PopActionConfig 등
- FilterOperator, AggregationType, SortConfig 타입
Phase 1: pop-dashboard 컴포넌트
- 4개 서브타입: KpiCard, ChartItem, GaugeItem, StatCard
- 4개 표시모드: ArrowsMode, AutoSlideMode, GridMode, ScrollMode
- 설정패널(PopDashboardConfig), 미리보기(PopDashboardPreview)
- 계산식 엔진(formula.ts), 데이터 조회(dataFetcher.ts)
- 팔레트/렌더러/타입 시스템 연동
fix(pop-text): DateTimeDisplay isRealtime 기본값 true로 수정
EOF
2026-02-10 11:04:18 +09:00
|
|
|
try {
|
|
|
|
|
const schema = await dashboardApi.getTableSchema(tableName);
|
|
|
|
|
return schema.columns.map((col) => ({
|
|
|
|
|
name: col.name,
|
|
|
|
|
type: col.type,
|
|
|
|
|
udtName: col.udtName,
|
|
|
|
|
}));
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 14:22:30 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 목록 조회 (설정 패널 Combobox용)
|
|
|
|
|
* tableManagementApi.getTableList() 래핑
|
|
|
|
|
*
|
|
|
|
|
* @INFRA-EXTRACT: useDataSource 완성 후 교체 예정
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchTableList(): Promise<TableInfo[]> {
|
|
|
|
|
try {
|
|
|
|
|
const response = await tableManagementApi.getTableList();
|
|
|
|
|
if (response.success && response.data) {
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|