2026-01-08 17:05:27 +09:00
|
|
|
/**
|
|
|
|
|
* PivotGrid 집계 함수 유틸리티
|
|
|
|
|
* 다양한 집계 연산을 수행합니다.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { AggregationType, PivotFieldFormat } from "../types";
|
|
|
|
|
|
|
|
|
|
// ==================== 집계 함수 ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 합계 계산
|
|
|
|
|
*/
|
|
|
|
|
export function sum(values: number[]): number {
|
|
|
|
|
return values.reduce((acc, val) => acc + (val || 0), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 개수 계산
|
|
|
|
|
*/
|
|
|
|
|
export function count(values: any[]): number {
|
|
|
|
|
return values.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 평균 계산
|
|
|
|
|
*/
|
|
|
|
|
export function avg(values: number[]): number {
|
|
|
|
|
if (values.length === 0) return 0;
|
|
|
|
|
return sum(values) / values.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 최소값 계산
|
|
|
|
|
*/
|
|
|
|
|
export function min(values: number[]): number {
|
|
|
|
|
if (values.length === 0) return 0;
|
|
|
|
|
return Math.min(...values.filter((v) => v !== null && v !== undefined));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 최대값 계산
|
|
|
|
|
*/
|
|
|
|
|
export function max(values: number[]): number {
|
|
|
|
|
if (values.length === 0) return 0;
|
|
|
|
|
return Math.max(...values.filter((v) => v !== null && v !== undefined));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 고유값 개수 계산
|
|
|
|
|
*/
|
|
|
|
|
export function countDistinct(values: any[]): number {
|
|
|
|
|
return new Set(values.filter((v) => v !== null && v !== undefined)).size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 집계 타입에 따른 집계 수행
|
|
|
|
|
*/
|
|
|
|
|
export function aggregate(
|
|
|
|
|
values: any[],
|
|
|
|
|
type: AggregationType = "sum"
|
|
|
|
|
): number {
|
|
|
|
|
const numericValues = values
|
|
|
|
|
.map((v) => (typeof v === "number" ? v : parseFloat(v)))
|
|
|
|
|
.filter((v) => !isNaN(v));
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "sum":
|
|
|
|
|
return sum(numericValues);
|
|
|
|
|
case "count":
|
|
|
|
|
return count(values);
|
|
|
|
|
case "avg":
|
|
|
|
|
return avg(numericValues);
|
|
|
|
|
case "min":
|
|
|
|
|
return min(numericValues);
|
|
|
|
|
case "max":
|
|
|
|
|
return max(numericValues);
|
|
|
|
|
case "countDistinct":
|
|
|
|
|
return countDistinct(values);
|
|
|
|
|
default:
|
|
|
|
|
return sum(numericValues);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== 포맷 함수 ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 숫자 포맷팅
|
|
|
|
|
*/
|
|
|
|
|
export function formatNumber(
|
|
|
|
|
value: number | null | undefined,
|
|
|
|
|
format?: PivotFieldFormat
|
|
|
|
|
): string {
|
|
|
|
|
if (value === null || value === undefined) return "-";
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
type = "number",
|
|
|
|
|
precision = 0,
|
|
|
|
|
thousandSeparator = true,
|
|
|
|
|
prefix = "",
|
|
|
|
|
suffix = "",
|
|
|
|
|
} = format || {};
|
|
|
|
|
|
|
|
|
|
let formatted: string;
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "currency":
|
|
|
|
|
formatted = value.toLocaleString("ko-KR", {
|
|
|
|
|
minimumFractionDigits: precision,
|
|
|
|
|
maximumFractionDigits: precision,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "percent":
|
|
|
|
|
formatted = (value * 100).toLocaleString("ko-KR", {
|
|
|
|
|
minimumFractionDigits: precision,
|
|
|
|
|
maximumFractionDigits: precision,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
default:
|
|
|
|
|
if (thousandSeparator) {
|
|
|
|
|
formatted = value.toLocaleString("ko-KR", {
|
|
|
|
|
minimumFractionDigits: precision,
|
|
|
|
|
maximumFractionDigits: precision,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
formatted = value.toFixed(precision);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${prefix}${formatted}${suffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 날짜 포맷팅
|
|
|
|
|
*/
|
|
|
|
|
export function formatDate(
|
|
|
|
|
value: Date | string | null | undefined,
|
|
|
|
|
format: string = "YYYY-MM-DD"
|
|
|
|
|
): string {
|
|
|
|
|
if (!value) return "-";
|
|
|
|
|
|
|
|
|
|
const date = typeof value === "string" ? new Date(value) : value;
|
|
|
|
|
|
|
|
|
|
if (isNaN(date.getTime())) return "-";
|
|
|
|
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
const quarter = Math.ceil((date.getMonth() + 1) / 3);
|
|
|
|
|
|
|
|
|
|
return format
|
|
|
|
|
.replace("YYYY", String(year))
|
|
|
|
|
.replace("MM", month)
|
|
|
|
|
.replace("DD", day)
|
|
|
|
|
.replace("Q", `Q${quarter}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 집계 타입 라벨 반환
|
|
|
|
|
*/
|
|
|
|
|
export function getAggregationLabel(type: AggregationType): string {
|
|
|
|
|
const labels: Record<AggregationType, string> = {
|
|
|
|
|
sum: "합계",
|
|
|
|
|
count: "개수",
|
|
|
|
|
avg: "평균",
|
|
|
|
|
min: "최소",
|
|
|
|
|
max: "최대",
|
|
|
|
|
countDistinct: "고유값",
|
|
|
|
|
};
|
|
|
|
|
return labels[type] || "합계";
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 17:11:46 +09:00
|
|
|
|