[ST] refactor: input - 자동 계산 cell 부동소수점 문제 해결을 위해 decimal.js 라이브러리 추가 및 계산 로직 수정, 6-2 지표 로직 수정

This commit is contained in:
2026-08-25 16:04:31 +09:00
parent 043fd3d4a8
commit 64ed53cf4d
11 changed files with 449 additions and 399 deletions
+1
View File
@@ -41,6 +41,7 @@
"chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13",
"decimal.js": "^10.6.0",
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.474.0",
@@ -7,6 +7,7 @@ import { RealGridReact, RGDataColumn } from "realgrid-react"
import type { CellStyleCallback, CellStyleCallbackParams,CellStyleCallbackResult, DataGridProps, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo } from "../type/DataGrid.type"
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
function mergeCellStyleCallbackResult(
baseStyleName: string | undefined,
@@ -32,7 +33,7 @@ declare module "realgrid" {
setOptions(options: {
footer?: { visible?: boolean };
summaryMode?: 'aggregate' | 'statistical' | 'none';
checkBar?: { visible?: boolean };
checkBar?: { visible?: boolean; checkableOnly?: boolean };
}): void;
}
}
@@ -49,10 +50,11 @@ type GridViewWithCheckApi = GridViewWithSetColumns & {
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
checkAll?: (checked: boolean) => void;
setCheckable?: (itemIndex: number, checkable: boolean) => void;
};
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
return columns.map((col) => {
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
fieldName: col.fieldName,
@@ -68,7 +70,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
}
}
return field;
@@ -138,6 +140,7 @@ const CompactDataGrid = ({
onRowClick,
onItemChecked,
onItemAllChecked,
checkableCallback,
compactFullWidth = false,
compactGridHeightClass = "h-[55vh]",
headerHeight = 60,
@@ -147,9 +150,11 @@ const CompactDataGrid = ({
const columnsRef = useRef(columns);
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
const checkableCallbackRef = useRef<DataGridProps["checkableCallback"]>(checkableCallback);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
checkableCallbackRef.current = checkableCallback;
useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return;
@@ -178,8 +183,10 @@ const CompactDataGrid = ({
try {
checkGrid.checkAll?.(false);
rowData.forEach((row, itemIndex) => {
const checkable = checkableCallbackRef.current?.(row, itemIndex) ?? true;
checkGrid.setCheckable?.(itemIndex, checkable);
const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
if (!checked) return;
if (!checked || !checkable) return;
if (checkGrid.checkItem) {
checkGrid.checkItem(itemIndex, true, false);
return;
@@ -276,7 +283,7 @@ const CompactDataGrid = ({
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
checkBar: { visible: showHeaderCheckbox },
checkBar: { visible: showHeaderCheckbox, checkableOnly: true },
rowIndicator: { visible: false },
});
@@ -499,6 +506,7 @@ const CompactDataGrid = ({
compactFullWidth,
compactGridHeightClass,
headerHeight,
checkableCallback,
]);
// 행 클릭 이벤트
@@ -527,6 +535,17 @@ const CompactDataGrid = ({
if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => {
const row = rowDataRef.current[itemIndex];
if (
row &&
checkableCallbackRef.current &&
!checkableCallbackRef.current(row, itemIndex)
) {
if (checked) {
(_grid as GridViewWithCheckApi).checkItem?.(itemIndex, false, false);
}
return;
}
onItemChecked(itemIndex, checked);
};
} else {
+32 -104
View File
@@ -24,6 +24,7 @@ import type {
} from "../type/DataGrid.type";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber";
import {
buildExcelExportFileBaseName,
buildFixedColumnNameSet,
@@ -251,7 +252,7 @@ declare module "realgrid" {
setOptions(options: {
footer?: { visible?: boolean };
summaryMode?: "aggregate" | "statistical" | "none";
checkBar?: { visible?: boolean; exclusive?: boolean };
checkBar?: { visible?: boolean; exclusive?: boolean; syncHeadCheck?: boolean };
}): void;
}
}
@@ -409,7 +410,13 @@ function reapplyListEditorsOnGrid(
type GridViewWithCheckApi = {
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
checkAll?: (checked: boolean) => void;
checkAll?: (
checked: boolean,
visibleOnly?: boolean,
checkableOnly?: boolean,
checkEvent?: boolean,
) => void;
setAllCheck?: (checked: boolean) => void;
};
function isRowCheckedFlag(value: unknown): boolean {
@@ -689,26 +696,6 @@ function sortRowsGroupPreserving(
}
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
if (!dataFormat) return null;
const match = dataFormat.match(/\.(0+|#+)/);
const decimalToken = match?.[1];
if (decimalToken) return decimalToken.length;
// #,##0 등 정수 포맷 — 소수부 없음 → 0자리 절삭(내림)
if (/[#0]/.test(dataFormat)) return 0;
return null;
}
function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
if (!Number.isFinite(value)) return 0;
if (decimalPlaces < 0) return value;
const factor = 10 ** decimalPlaces;
const scaled = value * factor;
// 절삭(truncate): 반올림 없이 소수점 이하를 버린다.
const truncated = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
return truncated / factor;
}
function buildFieldsFromColumns(
columns: {
fieldName: string;
@@ -823,33 +810,10 @@ function buildFieldsFromColumns(
hasQuotientRatioPercent ||
hasQuotientTimesMultiplierPercent
) {
const computedCallback = getComputedValueCallback(
col.computed,
) as (
prod: unknown,
dataRow: number,
fieldName: string,
fieldNames: string[],
values: unknown[],
) => number;
const decimalPlaces = getDecimalPlacesFromDataFormat(col.dataFormat);
field.valueCallback = (
prod: unknown,
dataRow: number,
fieldName: string,
fieldNames: string[],
values: unknown[],
) => {
const computedValue = computedCallback(
prod,
dataRow,
fieldName,
fieldNames,
values,
);
if (decimalPlaces == null) return computedValue;
return truncateByDecimalPlaces(computedValue, decimalPlaces);
};
field.valueCallback = wrapWithFormatTruncate(
getComputedValueCallback(col.computed),
col.dataFormat,
);
}
}
return field;
@@ -1047,7 +1011,17 @@ const DataGrid = ({
if (!dp?.setRows) return;
try { gv?.commit?.(); } catch { try { gv?.cancel?.(); } catch { /* noop */ } }
dp.setRows(rowData);
}, [rowData]);
if (showHeaderCheckbox) {
const checkGrid = gridRef.current?.gridView as GridViewWithCheckApi | undefined;
isSyncingChecksFromRowDataRef.current = true;
try {
checkGrid?.checkAll?.(false, false, false, false);
checkGrid?.setAllCheck?.(false);
} finally {
isSyncingChecksFromRowDataRef.current = false;
}
}
}, [rowData, showHeaderCheckbox]);
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
@@ -1634,7 +1608,7 @@ useEffect(() => {
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
checkBar: { visible: showHeaderCheckbox, exclusive: false },
checkBar: { visible: showHeaderCheckbox, exclusive: false, syncHeadCheck: true },
rowIndicator: { visible: false },
});
@@ -2054,14 +2028,10 @@ useEffect(() => {
gridView.refresh?.();
}, [footerVerificationResult, columns]);
// 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식)
// 행·셀 클릭 이벤트 (검증 오류 툴팁은 hover만 — onShowTooltip / 빈 셀 오버레이)
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (
!gridView ||
(!onRowClick && !cellTooltipCallback && !onDataCellClick)
)
return;
if (!gridView || (!onRowClick && !onDataCellClick)) return;
gridView.onCellClicked = (grid, clickData) => {
if (
@@ -2082,46 +2052,6 @@ useEffect(() => {
).getCurrent?.();
const fieldName =
cellIndex?.fieldName ?? cellIndex?.column ?? "";
// cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex)
// 빈 셀은 hover 오버레이로만 표시하므로 클릭 showMessage는 생략 (이중 표출 방지)
if (cellTooltipCallbackRef.current && row && cellIndex) {
if (!isEmptyGridCellValue(row[fieldName])) {
const msg = cellTooltipCallbackRef.current({
row: row as Record<string, string | number>,
fieldName,
});
if (msg) {
const colName =
columns.find(
(c) =>
c.fieldName === fieldName ||
c.name === cellIndex.column,
)?.name ??
cellIndex.column ??
fieldName;
(
gridView as {
showMessage?: (
opts: {
message: string;
hideDuration?: number;
},
idx: {
itemIndex: number;
column: string;
},
) => void;
}
).showMessage?.(
{ message: msg, hideDuration: 5000 },
{
itemIndex: cellIndex.itemIndex,
column: colName,
},
);
}
}
}
if (onDataCellClickRef.current && row !== undefined) {
onDataCellClickRef.current({
row: (row ?? {}) as Record<string, string | number>,
@@ -2139,7 +2069,7 @@ useEffect(() => {
return () => {
if (gridView.onCellClicked) gridView.onCellClicked = undefined;
};
}, [onRowClick, onDataCellClick, cellTooltipCallback, columns]);
}, [onRowClick, onDataCellClick]);
// RealGrid 네이티브 툴팁은 데이터 셀만 지원 → 빈 셀·총계 footer는 React 오버레이로 보완
useEffect(() => {
@@ -2404,15 +2334,13 @@ useEffect(() => {
const gridView = gridRef.current.gridView as GridViewWithCheckApi;
const hasCheckedColumn = rowData.some((row) =>
Object.prototype.hasOwnProperty.call(row, "__checked"),
);
if (!hasCheckedColumn) return;
// rowData 교체(조회/저장/제출/항목삭제) 시 헤더·행 체크를 해제한다.
// `__checked`가 있는 행만 다시 체크한다. 없으면 전부 해제 상태로 둔다.
const timer = window.setTimeout(() => {
isSyncingChecksFromRowDataRef.current = true;
try {
gridView.checkAll?.(false);
gridView.checkAll?.(false, false, false, false);
gridView.setAllCheck?.(false);
rowData.forEach((row, itemIndex) => {
if (!isRowCheckedFlag((row as Record<string, unknown>).__checked)) return;
if (gridView.checkItem) {
@@ -16,6 +16,7 @@ const SplitDataGrid = ({
leftCellStyleCallback,
rightOnItemChecked,
rightOnItemAllChecked,
rightCheckableCallback,
enableSort = false,
}: SplitDataGridProps) => {
return (
@@ -39,6 +40,7 @@ const SplitDataGrid = ({
setRowData={setRightRowData}
onItemChecked={rightOnItemChecked}
onItemAllChecked={rightOnItemAllChecked}
checkableCallback={rightCheckableCallback}
/>
</div>
);
@@ -44,6 +44,7 @@ function mergeCellStyleCallbackResult(
import SplitDataGrid from "./SplitDataGrid";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
import {
buildExcelExportFileBaseName,
buildFixedColumnNameSet,
@@ -252,7 +253,7 @@ function sortRowsGroupPreserving(
}
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
return columns.map((col) => {
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
fieldName: col.fieldName,
@@ -268,7 +269,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
}
}
return field;
@@ -312,6 +312,28 @@ const PublicSewageFacilityInp = () => {
return v === 1 || v === '1' || v === 'Y' || v === 'y' || v === 'true';
};
const isSelfMappingTarget = (
row: Record<string, string | number>,
mainFcltSn: string | null,
): boolean => Boolean(mainFcltSn) && String(row.fcltSn ?? '') === mainFcltSn;
const hasCheckedSelfMapping = (
rows: Record<string, string | number>[],
mainFcltSn: string | null,
): boolean => rows.some((r) => isSelfMappingTarget(r, mainFcltSn) && isCheckedRow(r));
/** 왼쪽에서 선택한 시설을 오른쪽에서 본인 체크하면 다른 시설은 해제 */
const applyExclusiveSelfMappingChecks = (
rows: Record<string, string | number>[],
mainFcltSn: string | null,
): Record<string, string | number>[] => {
if (!hasCheckedSelfMapping(rows, mainFcltSn)) return rows;
return rows.map((r) => ({
...r,
__checked: isSelfMappingTarget(r, mainFcltSn) ? 'Y' : '',
}));
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
@@ -520,19 +542,36 @@ const PublicSewageFacilityInp = () => {
return;
}
const saveList: PublicSewageMainFacilityMappingSaveItem[] = rightRows
.filter((r) => isCheckedRow(r))
.map((r) => ({
fcltSn: nullableTrimmedString(r.fcltSn),
admdstCd: nullableTrimmedString(r.admdstCd),
admdstNm: nullableTrimmedString(r.admdstNm),
swtrmtFcltCd: nullableTrimmedString(r.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(r.swtrmtFcltNm),
isMainYn: nullableTrimmedString(r.isMainYn),
}));
const checkedRows = rightRows.filter((r) => isCheckedRow(r));
const exclusiveSelf = hasCheckedSelfMapping(rightRows, selectedMainFacilitySn);
const saveList: PublicSewageMainFacilityMappingSaveItem[] = (
exclusiveSelf
? checkedRows.filter((r) => isSelfMappingTarget(r, selectedMainFacilitySn))
: checkedRows
).map((r) => ({
fcltSn: nullableTrimmedString(r.fcltSn),
admdstCd: nullableTrimmedString(r.admdstCd),
admdstNm: nullableTrimmedString(r.admdstNm),
swtrmtFcltCd: nullableTrimmedString(r.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(r.swtrmtFcltNm),
isMainYn: nullableTrimmedString(r.isMainYn),
}));
const exclusiveDroppedSns = exclusiveSelf
? new Set(
checkedRows
.filter((r) => !isSelfMappingTarget(r, selectedMainFacilitySn))
.map((r) => String(r.fcltSn ?? ''))
.filter((sn) => sn !== ''),
)
: new Set<string>();
const deleteList = rightRows
.filter((r) => String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r))
.filter((r) => {
const sn = String(r.fcltSn ?? '');
if (String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r)) return true;
return exclusiveDroppedSns.has(sn) && String(r.mappedYn ?? '') === 'Y';
})
.map((r) => String(r.fcltSn ?? ''))
.filter((sn) => sn !== '');
@@ -1132,10 +1171,13 @@ const PublicSewageFacilityInp = () => {
if (rawList.length === 0) {
alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.');
}
const mappedTargetRows = rawList.map((item) => ({
...item,
__checked: item.mappedYn === 'Y' ? 'Y' : '',
}));
const mappedTargetRows = applyExclusiveSelfMappingChecks(
rawList.map((item) => ({
...item,
__checked: item.mappedYn === 'Y' ? 'Y' : '',
})),
fcltSn,
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacilityTarget', toGridRows(mappedTargetRows));
@@ -1318,11 +1360,25 @@ const PublicSewageFacilityInp = () => {
const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])];
if (!rows[itemIndex]) return prev;
const clicked = rows[itemIndex];
if (
checked &&
hasCheckedSelfMapping(rows, selectedMainFacilitySn) &&
!isSelfMappingTarget(clicked, selectedMainFacilitySn)
) {
return prev;
}
rows[itemIndex] = {
...rows[itemIndex],
...clicked,
__checked: checked ? 'Y' : '',
};
next.set('MainFacilityTarget', rows);
next.set(
'MainFacilityTarget',
applyExclusiveSelfMappingChecks(
rows,
selectedMainFacilitySn,
),
);
return next;
});
}
@@ -1333,16 +1389,59 @@ const PublicSewageFacilityInp = () => {
? (checked) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({
...r,
__checked: checked ? 'Y' : '',
}));
next.set('MainFacilityTarget', rows);
const rows = [...(next.get('MainFacilityTarget') ?? [])];
if (
checked &&
hasCheckedSelfMapping(rows, selectedMainFacilitySn)
) {
next.set(
'MainFacilityTarget',
applyExclusiveSelfMappingChecks(
rows,
selectedMainFacilitySn,
),
);
return next;
}
next.set(
'MainFacilityTarget',
rows.map((r) => ({
...r,
__checked:
checked &&
!isSelfMappingTarget(
r,
selectedMainFacilitySn,
)
? 'Y'
: '',
})),
);
return next;
});
}
: undefined
}
rightCheckableCallback={
tab.tabKey === 'ST00060002'
? (row) => {
const rows =
rowDataMap.get('MainFacilityTarget') ?? [];
if (
!hasCheckedSelfMapping(
rows,
selectedMainFacilitySn,
)
) {
return true;
}
return isSelfMappingTarget(
row,
selectedMainFacilitySn,
);
}
: undefined
}
setLeftRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
@@ -418,6 +418,11 @@ export type DataGridProps = {
onRowsChange?: (rows: Record<string, string | number>[]) => void;
fixedColumns?: number; // 고정할 컬럼 개수 (좌측부터)
showHeaderCheckbox?: boolean; // 헤더 체크박스 표시 여부 (기본값: true)
/** CheckBar 행별 체크 가능 여부 (false면 해당 행 체크박스 비활성) */
checkableCallback?: (
row: Record<string, string | number>,
itemIndex: number,
) => boolean;
rowData: Record<string, string | number>[];
setRowData: React.Dispatch<
React.SetStateAction<Record<string, string | number>[]>
@@ -532,6 +537,11 @@ export type SplitDataGridProps = {
rightOnItemChecked?: (itemIndex: number, checked: boolean) => void;
/** 우측 분할 그리드 헤더 전체 체크 변경 */
rightOnItemAllChecked?: (checked: boolean) => void;
/** 우측 분할 그리드 행별 체크 가능 여부 */
rightCheckableCallback?: (
row: Record<string, string | number>,
itemIndex: number,
) => boolean;
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
@@ -70,7 +70,7 @@ export const MainFacilityTargetColumn = {
export const MainFacilityInpColumn = {
tabKey: 'ST00060002',
tabLabel: '중심 처리 시설',
tabLabel: '위탁 관리 입력',
isSplitGrid: true,
left: MainFacilityColumn,
right: MainFacilityTargetColumn,
@@ -1,4 +1,13 @@
import type { ComputedFieldConfig,DataFieldValueCallback } from "@/components/type/DataGrid.type";
import Decimal from "decimal.js";
import type { ComputedFieldConfig, DataFieldValueCallback } from "@/components/type/DataGrid.type";
import {
decimalToGridNumber,
sumDecimals,
toDecimal,
truncDecimal,
} from "@/components/util/grid/decimalNumber";
/** 값 → 숫자 변환 (null/undefined/빈문자열 → 0) */
function toNum(v: unknown): number {
@@ -8,78 +17,41 @@ function toNum(v: unknown): number {
return isNaN(n) ? 0 : n;
}
/** 소수점 첫째 자리 이후 버림 (반올림 없음) */
function truncateToFirstDecimal(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.floor(value * 10) / 10;
}
/** computed sum용 스케일 (표시 최대 소수 3자리 + 여유) */
const SUM_DECIMAL_SCALE = 1e6;
/**
* 스케일 정수 합을 Number로 되돌릴 때 생기는 IEEE 오차 보정.
* 39057.34는 이진수로 39057.33999… 가 되어 RealGrid `#,##0.00` 절사 시 39057.33이 된다.
* 1e-12는 이 값에서 *100 경계를 넘지 못하므로, 값 크기에 비례한 ULP만큼 밀어 올린다.
*/
function numberFromScaledSum(scaledSum: number): number {
if (scaledSum === 0) return 0;
const raw = scaledSum / SUM_DECIMAL_SCALE;
const ulp = Number.EPSILON * Math.max(1, Math.abs(raw));
return raw + Math.sign(raw) * ulp * 8;
}
/**
* 각 값을 소수 6자리 정수로 올린 뒤 더한 다음 다시 나눈다.
* 예: 2712.5 + 6291.13 → (2712500000 + 6291130000) / 1e6 = 9003.63
*/
function sumViaScaledIntegers(nums: number[]): number {
let scaledSum = 0;
for (const n of nums) {
if (!Number.isFinite(n)) continue;
scaledSum += Math.round(n * SUM_DECIMAL_SCALE);
}
return numberFromScaledSum(scaledSum);
}
/** 합계용 valueCallback 생성 */
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const nums: number[] = [];
for (const f of fieldNames) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) nums.push(toNum(values[idx]));
}
return sumViaScaledIntegers(nums);
};
}
/** 평균용 valueCallback 생성 */
export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const nums: number[] = [];
for (const f of fieldNames) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) {
nums.push(toNum(values[idx]));
}
}
if (nums.length === 0) return 0;
return sumViaScaledIntegers(nums) / nums.length;
};
}
function sumFieldValues(
function fieldNums(
fields: string[],
fieldNamesArr: string[],
values: unknown[],
): number {
): number[] {
const nums: number[] = [];
for (const f of fields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) nums.push(toNum(values[idx]));
}
return sumViaScaledIntegers(nums);
return nums;
}
function fieldDecimal(
field: string,
fieldNamesArr: string[],
values: unknown[],
): Decimal {
const idx = fieldNamesArr.indexOf(field);
return toDecimal(idx >= 0 ? toNum(values[idx]) : 0);
}
/** 합계용 valueCallback 생성 */
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) =>
decimalToGridNumber(sumDecimals(fieldNums(fieldNames, fieldNamesArr, values)));
}
/** 평균용 valueCallback 생성 */
export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const nums = fieldNums(fieldNames, fieldNamesArr, values);
if (nums.length === 0) return 0;
return decimalToGridNumber(sumDecimals(nums).div(nums.length));
};
}
/** halfSumOfSums 한 건 — (sum(first) + sum(second)) / 2, 소수 내림 */
@@ -89,9 +61,9 @@ function computeHalfSumOfSums(
fieldNamesArr: string[],
values: unknown[],
): number {
const firstSum = sumFieldValues(firstSumFields, fieldNamesArr, values);
const secondSum = sumFieldValues(secondSumFields, fieldNamesArr, values);
return Math.floor((firstSum + secondSum) / 2);
const firstSum = sumDecimals(fieldNums(firstSumFields, fieldNamesArr, values));
const secondSum = sumDecimals(fieldNums(secondSumFields, fieldNamesArr, values));
return firstSum.plus(secondSum).div(2).floor().toNumber();
}
export type HalfSumOfSumsGroup = {
@@ -134,11 +106,9 @@ export function createDifferenceValueCallback(fields: string[]): DataFieldValueC
const b = fields[1] ?? "";
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
if (!a || !b) return 0;
const ia = fieldNamesArr.indexOf(a);
const ib = fieldNamesArr.indexOf(b);
const va = ia >= 0 ? toNum(values[ia]) : 0;
const vb = ib >= 0 ? toNum(values[ib]) : 0;
return va - vb;
return decimalToGridNumber(
fieldDecimal(a, fieldNamesArr, values).minus(fieldDecimal(b, fieldNamesArr, values)),
);
};
}
@@ -148,15 +118,9 @@ export function createDifferenceOfSumsValueCallback(
subtrahendFields: string[],
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const sumOf = (fields: string[]) => {
let sum = 0;
for (const f of fields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sum += toNum(values[idx]);
}
return sum;
};
return sumOf(minuendFields) - sumOf(subtrahendFields);
const minuend = sumDecimals(fieldNums(minuendFields, fieldNamesArr, values));
const subtrahend = sumDecimals(fieldNums(subtrahendFields, fieldNamesArr, values));
return decimalToGridNumber(minuend.minus(subtrahend));
};
}
@@ -181,14 +145,9 @@ export function createMaxOfFieldAndSumValueCallback(
sumFields: string[],
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const baseIdx = fieldNamesArr.indexOf(baseField);
const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0;
let sumVal = 0;
for (const f of sumFields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sumVal += toNum(values[idx]);
}
return Math.max(baseVal, sumVal);
const baseVal = fieldDecimal(baseField, fieldNamesArr, values);
const sumVal = sumDecimals(fieldNums(sumFields, fieldNamesArr, values));
return decimalToGridNumber(Decimal.max(baseVal, sumVal));
};
}
@@ -197,18 +156,13 @@ export function createSumOfMaxOfFieldAndSumGroupsValueCallback(
groups: Array<{ baseField: string; sumFields: string[] }>,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
let total = 0;
let total = new Decimal(0);
for (const group of groups) {
const baseIdx = fieldNamesArr.indexOf(group.baseField);
const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0;
let sumVal = 0;
for (const f of group.sumFields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sumVal += toNum(values[idx]);
}
total += Math.max(baseVal, sumVal);
const baseVal = fieldDecimal(group.baseField, fieldNamesArr, values);
const sumVal = sumDecimals(fieldNums(group.sumFields, fieldNamesArr, values));
total = total.plus(Decimal.max(baseVal, sumVal));
}
return total;
return decimalToGridNumber(total);
};
}
@@ -218,17 +172,10 @@ export function createCoverageRateValueCallback(
denominatorFields: string[]
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const getSum = (fields: string[]) => {
let sum = 0;
for (const f of fields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sum += toNum(values[idx]);
}
return sum;
};
const total2 = getSum(numeratorFields);
const total1 = getSum(denominatorFields);
return total1 === 0 ? 0 : (total2 / total1) * 100;
const total2 = sumDecimals(fieldNums(numeratorFields, fieldNamesArr, values));
const total1 = sumDecimals(fieldNums(denominatorFields, fieldNamesArr, values));
if (total1.isZero()) return 0;
return decimalToGridNumber(total2.div(total1).times(100));
};
}
@@ -239,28 +186,27 @@ export function createSwrgInstlrtRateValueCallback(
thirdFields: string[],
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const trunc1 = (d: Decimal) => truncDecimal(d, 1);
const getSum = (fields: string[]) => {
let sum = 0;
let sum = new Decimal(0);
for (const f of fields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) {
sum += truncateToFirstDecimal(toNum(values[idx]));
sum = sum.plus(trunc1(toDecimal(toNum(values[idx]))));
}
}
return truncateToFirstDecimal(sum);
return trunc1(sum);
};
const sum1 = getSum(numeratorFields);
const sum2 = getSum(denominatorFields);
const piplSupprt = trunc1(fieldDecimal(thirdFields[0] || "piplSupprt", fieldNamesArr, values));
const piplSupprt = truncateToFirstDecimal(
toNum(values[fieldNamesArr.indexOf(thirdFields[0] || "piplSupprt")]),
);
const ratioPercent = sum1.isZero()
? new Decimal(0)
: trunc1(sum2.div(sum1).times(100));
const result = trunc1(ratioPercent.times(piplSupprt).div(100));
const ratioPercent =
sum1 === 0 ? 0 : truncateToFirstDecimal((sum2 / sum1) * 100);
const result = truncateToFirstDecimal((ratioPercent * piplSupprt) / 100);
return result;
return decimalToGridNumber(result);
};
}
@@ -270,12 +216,10 @@ export function createBodPrcsRtValueCallback(
dschgField: string,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const inflwIdx = fieldNamesArr.indexOf(inflwField);
const dschgIdx = fieldNamesArr.indexOf(dschgField);
const inflw = inflwIdx >= 0 ? toNum(values[inflwIdx]) : 0;
const dschg = dschgIdx >= 0 ? toNum(values[dschgIdx]) : 0;
if (inflw === 0) return 0;
return (inflw - dschg) / inflw;
const inflw = fieldDecimal(inflwField, fieldNamesArr, values);
const dschg = fieldDecimal(dschgField, fieldNamesArr, values);
if (inflw.isZero()) return 0;
return decimalToGridNumber(inflw.minus(dschg).div(inflw));
};
}
@@ -288,17 +232,14 @@ export function createPumpGrossPpqtyValueCallback(
}>,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
let total = 0;
let total = new Decimal(0);
for (const group of pumpGroups) {
const pumpIdx = fieldNamesArr.indexOf(group.pumpCntField);
const rsrvIdx = fieldNamesArr.indexOf(group.rsrvPumpCntField);
const perIdx = fieldNamesArr.indexOf(group.perPpqtyField);
const pumpCnt = pumpIdx >= 0 ? toNum(values[pumpIdx]) : 0;
const rsrvCnt = rsrvIdx >= 0 ? toNum(values[rsrvIdx]) : 0;
const perPpqty = perIdx >= 0 ? toNum(values[perIdx]) : 0;
total += (pumpCnt + rsrvCnt) * perPpqty;
const pumpCnt = fieldDecimal(group.pumpCntField, fieldNamesArr, values);
const rsrvCnt = fieldDecimal(group.rsrvPumpCntField, fieldNamesArr, values);
const perPpqty = fieldDecimal(group.perPpqtyField, fieldNamesArr, values);
total = total.plus(pumpCnt.plus(rsrvCnt).times(perPpqty));
}
return total;
return decimalToGridNumber(total);
};
}
@@ -310,18 +251,14 @@ export function createQuotientRatioPercentValueCallback(
const [numField, numDenField] = numeratorQuotient;
const [denNumField, denDenField] = denominatorQuotient;
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const getVal = (field: string) => {
const idx = fieldNamesArr.indexOf(field);
return idx >= 0 ? toNum(values[idx]) : 0;
};
const numDen = getVal(numDenField);
const denDen = getVal(denDenField);
const denNum = getVal(denNumField);
if (numDen === 0 || denDen === 0 || denNum === 0) return 0;
const numerator = getVal(numField) / numDen;
const denominator = denNum / denDen;
if (denominator === 0) return 0;
return (numerator / denominator) * 100;
const numDen = fieldDecimal(numDenField, fieldNamesArr, values);
const denDen = fieldDecimal(denDenField, fieldNamesArr, values);
const denNum = fieldDecimal(denNumField, fieldNamesArr, values);
if (numDen.isZero() || denDen.isZero() || denNum.isZero()) return 0;
const numerator = fieldDecimal(numField, fieldNamesArr, values).div(numDen);
const denominator = denNum.div(denDen);
if (denominator.isZero()) return 0;
return decimalToGridNumber(numerator.div(denominator).times(100));
};
}
@@ -331,11 +268,10 @@ export function createSumPerTenThousandValueCallback(
denominatorField: string,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const sum = sumFieldValues(numeratorFields, fieldNamesArr, values);
const denIdx = fieldNamesArr.indexOf(denominatorField);
const den = denIdx >= 0 ? toNum(values[denIdx]) : 0;
if (den === 0) return 0;
return truncateToFirstDecimal((sum / den) * 10000);
const den = fieldDecimal(denominatorField, fieldNamesArr, values);
if (den.isZero()) return 0;
const sum = sumDecimals(fieldNums(numeratorFields, fieldNamesArr, values));
return decimalToGridNumber(truncDecimal(sum.div(den).times(10000), 1));
};
}
@@ -345,12 +281,11 @@ export function createQuotientValueCallback(fields: string[]): DataFieldValueCal
const denominatorField = fields[1] ?? "";
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
if (!numeratorField || !denominatorField) return 0;
const numIdx = fieldNamesArr.indexOf(numeratorField);
const denIdx = fieldNamesArr.indexOf(denominatorField);
const numerator = numIdx >= 0 ? toNum(values[numIdx]) : 0;
const denominator = denIdx >= 0 ? toNum(values[denIdx]) : 0;
if (denominator === 0) return 0;
return numerator / denominator;
const denominator = fieldDecimal(denominatorField, fieldNamesArr, values);
if (denominator.isZero()) return 0;
return decimalToGridNumber(
fieldDecimal(numeratorField, fieldNamesArr, values).div(denominator),
);
};
}
@@ -361,15 +296,11 @@ export function createQuotientTimesMultiplierPercentValueCallback(
multiplierField: string,
): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
const getVal = (field: string) => {
const idx = fieldNamesArr.indexOf(field);
return idx >= 0 ? toNum(values[idx]) : 0;
};
const denominator = getVal(denominatorField);
if (denominator === 0) return 0;
const quotient = getVal(numeratorField) / denominator;
const multiplier = getVal(multiplierField);
return truncateToFirstDecimal(quotient * multiplier * 100);
const denominator = fieldDecimal(denominatorField, fieldNamesArr, values);
if (denominator.isZero()) return 0;
const quotient = fieldDecimal(numeratorField, fieldNamesArr, values).div(denominator);
const multiplier = fieldDecimal(multiplierField, fieldNamesArr, values);
return decimalToGridNumber(truncDecimal(quotient.times(multiplier).times(100), 1));
};
}
@@ -0,0 +1,100 @@
import Decimal from "decimal.js";
import type { DataFieldValueCallback } from "@/components/type/DataGrid.type";
/** IEEE Number를 최단 십진 문자열로 복원 (69.96 → "69.96") */
export function toDecimal(n: number): Decimal {
if (!Number.isFinite(n) || n === 0) return new Decimal(0);
return new Decimal(n.toString());
}
function ulpOf(n: number): number {
return Number.EPSILON * Math.max(1, Math.abs(n));
}
/** RealGrid `#,##0.0` / `.00` / `.000` 절사가 의도한 십진값보다 작아지는지 */
function needsDisplayNudge(n: number, intended: Decimal): boolean {
for (const places of [1, 2, 3] as const) {
const factor = 10 ** places;
const scaled = n * factor;
const ieeeTrunc = n >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
const intendedTrunc = intended.times(new Decimal(10).pow(places)).trunc();
if (n >= 0 && new Decimal(ieeeTrunc).lt(intendedTrunc)) return true;
if (n < 0 && new Decimal(ieeeTrunc).gt(intendedTrunc)) return true;
}
return false;
}
/**
* Decimal → RealGrid Number.
* 18856.1처럼 이진 근사가 실제보다 작으면 `#,##0.00` 절사 시 18856.09가 되므로
* 그때만 1 ULP씩 올려 표시를 맞춘다. 14384.26처럼 이미 충분한 값은 그대로 둔다.
*/
export function decimalToGridNumber(value: Decimal): number {
if (value.isZero()) return 0;
let n = value.toNumber();
const sign = Math.sign(n) || 1;
for (let i = 0; i < 16; i++) {
if (!needsDisplayNudge(n, value)) return n;
n += sign * ulpOf(n);
}
return n;
}
export function truncDecimal(value: Decimal, decimalPlaces: number): Decimal {
if (decimalPlaces < 0) return value;
const factor = new Decimal(10).pow(decimalPlaces);
return value.times(factor).trunc().div(factor);
}
/**
* 절삭. `18856.1 * 100`이 1885609.999…가 되어 Math.floor 시 18856.09가 되므로
* 최단 십진 문자열로 복원한 뒤 Decimal trunc, RealGrid 표시용으로 ULP 보정.
*/
export function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
if (!Number.isFinite(value)) return 0;
if (decimalPlaces < 0) return value;
return decimalToGridNumber(truncDecimal(toDecimal(value), decimalPlaces));
}
export function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
if (!dataFormat) return null;
const match = dataFormat.match(/\.(0+|#+)/);
const decimalToken = match?.[1];
if (decimalToken) return decimalToken.length;
if (/[#0]/.test(dataFormat)) return 0;
return null;
}
export function wrapWithFormatTruncate(
callback: DataFieldValueCallback,
dataFormat?: string,
): (
prod: unknown,
dataRow: number,
fieldName: string,
fieldNames: string[],
values: unknown[],
) => number {
const decimalPlaces = getDecimalPlacesFromDataFormat(dataFormat);
return (prod, dataRow, fieldName, fieldNames, values) => {
const computedValue = callback(prod, dataRow, fieldName, fieldNames, values);
const n = typeof computedValue === "number" ? computedValue : Number(computedValue);
const num = Number.isFinite(n) ? n : 0;
if (decimalPlaces == null) return num;
return truncateByDecimalPlaces(num, decimalPlaces);
};
}
export function sumDecimals(nums: number[]): Decimal {
let sum = new Decimal(0);
for (const n of nums) {
if (!Number.isFinite(n)) continue;
sum = sum.plus(toDecimal(n));
}
return sum;
}
export function sumToGridNumber(nums: number[]): number {
return decimalToGridNumber(sumDecimals(nums));
}
@@ -1,52 +1,51 @@
import type { DataColumn,GridView } from "@/components/type/DataGrid.type";
import type { DataColumn, GridView } from "@/components/type/DataGrid.type";
// 그리드 푸터 합계 계산 함수 (계산 로직)
export const getFooterSummary = (grid: GridView, column: DataColumn) => {
// 데이터 소스에서 직접 합계 계산
import {
decimalToGridNumber,
sumDecimals,
truncDecimal,
} from "@/components/util/grid/decimalNumber";
function toNumeric(cellValue: unknown): number | null {
if (cellValue == null || cellValue === "") return null;
const numericValue =
typeof cellValue === "string"
? Number(String(cellValue).replace(/,/g, ""))
: Number(cellValue);
if (!Number.isFinite(numericValue)) return null;
return numericValue;
}
function collectNumericValues(
grid: GridView,
column: DataColumn,
includeRow?: (rowIndex: number) => boolean,
): number[] {
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
let sum = 0;
const nums: number[] = [];
for (let i = 0; i < rowCount; i++) {
const cellValue = dataSource.getValue(i, column.fieldName);
// 문자열인 경우 숫자로 변환
const numericValue = typeof cellValue === 'string'
? Number(String(cellValue).replace(/,/g, ''))
: Number(cellValue);
if (!isNaN(numericValue) && numericValue !== null && numericValue !== undefined) {
sum += numericValue;
}
if (includeRow && !includeRow(i)) continue;
const n = toNumeric(dataSource.getValue(i, column.fieldName));
if (n != null) nums.push(n);
}
return sum;
};
return nums;
}
// 그리드 푸터 합계 계산 함수 (계산 로직)
export const getFooterSummary = (grid: GridView, column: DataColumn) =>
decimalToGridNumber(sumDecimals(collectNumericValues(grid, column)));
// 평균 계산 함수
export const getFooterAvgSummary = (grid: GridView, column: DataColumn) => {
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
let sum = 0;
let avg = 0;
for (let i = 0; i < rowCount; i++) {
const cellValue = dataSource.getValue(i, column.fieldName);
const numericValue = typeof cellValue === 'string'
? Number(String(cellValue).replace(/,/g, ''))
: Number(cellValue);
if (!isNaN(numericValue) && numericValue !== null && numericValue !== undefined) {
sum += numericValue;
}
}
avg = sum / rowCount;
return Math.floor(avg * 10) / 10;
}
const nums = collectNumericValues(grid, column);
const rowCount = grid.getDataSource().getRowCount();
if (rowCount === 0) return 0;
const avg = sumDecimals(nums).div(rowCount);
return decimalToGridNumber(truncDecimal(avg, 1));
};
// 구분 필드의 고유값들을 찾는 함수 (첫 번째, 두 번째). 기본 필드명 'displayYear'
const getDivisionValues = (grid: GridView, divisionFieldName: string = 'displayYear'): string[] => {
@@ -73,38 +72,21 @@ export const getFooterSummaryByDivision = (
divisionIndex: number,
divisionFieldName: string = 'displayYear'
) => {
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
let sum = 0;
// "구분" 필드의 고유값 찾기
const divisionValues = getDivisionValues(grid, divisionFieldName);
if (divisionValues.length <= divisionIndex) {
return 0; // 해당 인덱스의 고유값이 없으면 0 반환
return 0;
}
const targetDivision = divisionValues[divisionIndex];
const dataSource = grid.getDataSource();
for (let i = 0; i < rowCount; i++) {
// "구분" 필드 값 확인
const divisionValue = dataSource.getValue(i, divisionFieldName);
if (String(divisionValue) !== targetDivision) {
continue; // 해당 "구분" 값이 아니면 스킵
}
const cellValue = dataSource.getValue(i, column.fieldName);
// 문자열인 경우 숫자로 변환
const numericValue = typeof cellValue === 'string'
? Number(String(cellValue).replace(/,/g, ''))
: Number(cellValue);
if (!isNaN(numericValue) && numericValue !== null && numericValue !== undefined) {
sum += numericValue;
}
}
return sum;
return decimalToGridNumber(
sumDecimals(
collectNumericValues(grid, column, (i) =>
String(dataSource.getValue(i, divisionFieldName)) === targetDivision,
),
),
);
};
// 구분 필드 값에 따라 필터링하여 평균 계산하는 함수. 기본 필드명 'displayYear'
@@ -115,45 +97,22 @@ export const getFooterAvgSummaryByDivision = (
divisionIndex: number,
divisionFieldName: string = 'displayYear'
) => {
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
let sum = 0;
let count = 0;
// "구분" 필드의 고유값 찾기
const divisionValues = getDivisionValues(grid, divisionFieldName);
if (divisionValues.length <= divisionIndex) {
return 0; // 해당 인덱스의 고유값이 없으면 0 반환
return 0;
}
const targetDivision = divisionValues[divisionIndex];
const dataSource = grid.getDataSource();
const nums = collectNumericValues(grid, column, (i) =>
String(dataSource.getValue(i, divisionFieldName)) === targetDivision,
);
for (let i = 0; i < rowCount; i++) {
// "구분" 필드 값 확인
const divisionValue = dataSource.getValue(i, divisionFieldName);
if (String(divisionValue) !== targetDivision) {
continue; // 해당 "구분" 값이 아니면 스킵
}
const cellValue = dataSource.getValue(i, column.fieldName);
// 문자열인 경우 숫자로 변환
const numericValue = typeof cellValue === 'string'
? Number(String(cellValue).replace(/,/g, ''))
: Number(cellValue);
if (!isNaN(numericValue) && numericValue !== null && numericValue !== undefined) {
sum += numericValue;
count += 1;
}
}
if (count === 0) {
if (nums.length === 0) {
return 0;
}
const avg = sum / count;
return Math.floor(avg * 10) / 10;
return decimalToGridNumber(truncDecimal(sumDecimals(nums).div(nums.length), 1));
};
// Footer 설정 객체 생성 함수
@@ -249,4 +208,4 @@ export const footerDecimal3ArrayByDivision = createSumFooterArrayByDivision('#,#
export const footerAvgDecimal = createAvgFooter('#,##0.0');
// "구분" 값에 따라 필터링하는 평균 Footer 배열 (소수점 1자리, 2줄)
export const footerAvgDecimalArrayByDivision = createAvgFooterArrayByDivision('#,##0.0');
export const footerAvgDecimalArrayByDivision = createAvgFooterArrayByDivision('#,##0.0');