[st] feat: 통계 검증 리포트 화면 작업
This commit is contained in:
@@ -321,10 +321,14 @@ const DataGrid = ({
|
||||
cellStyleCallback,
|
||||
cellTooltipCallback,
|
||||
onRowClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
onScrollToBottom,
|
||||
statusInfo,
|
||||
highlightedRowId,
|
||||
idFieldName = "id",
|
||||
enableSort = false,
|
||||
gridHeight = 65, // vh 단위
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
@@ -340,9 +344,18 @@ const DataGrid = ({
|
||||
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
|
||||
cellTooltipCallback,
|
||||
);
|
||||
const onScrollToBottomRef = useRef<(() => void) | undefined>(
|
||||
onScrollToBottom,
|
||||
);
|
||||
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
|
||||
const onItemAllCheckedRef =
|
||||
useRef<typeof onItemAllChecked>(onItemAllChecked);
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
cellTooltipCallbackRef.current = cellTooltipCallback;
|
||||
onScrollToBottomRef.current = onScrollToBottom;
|
||||
onItemCheckedRef.current = onItemChecked;
|
||||
onItemAllCheckedRef.current = onItemAllChecked;
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
@@ -558,10 +571,10 @@ const DataGrid = ({
|
||||
) {
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
];
|
||||
if (
|
||||
cell &&
|
||||
@@ -637,7 +650,7 @@ const DataGrid = ({
|
||||
const col = range.s.c + colIndex;
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
@@ -761,13 +774,13 @@ const DataGrid = ({
|
||||
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
|
||||
(gridView.displayOptions as DisplayOptions).fitStyle =
|
||||
GridFitStyle.EVEN;
|
||||
@@ -937,7 +950,7 @@ const DataGrid = ({
|
||||
) {
|
||||
(
|
||||
column.footer[
|
||||
index
|
||||
index
|
||||
] as FooterItemWithCallback
|
||||
).valueCallback =
|
||||
footerItem.valueCallback;
|
||||
@@ -990,11 +1003,11 @@ const DataGrid = ({
|
||||
getGridDataRef.current = () => {
|
||||
const gv = gridRef.current?.gridView as
|
||||
| {
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
@@ -1090,6 +1103,47 @@ const DataGrid = ({
|
||||
};
|
||||
}, [onRowClick, cellTooltipCallback, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || !onScrollToBottom) return;
|
||||
|
||||
gridView.onScrollToBottom = () => {
|
||||
onScrollToBottomRef.current?.();
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (gridView.onScrollToBottom)
|
||||
gridView.onScrollToBottom = undefined;
|
||||
};
|
||||
}, [onScrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
if (onItemChecked) {
|
||||
gridView.onItemChecked = (_grid, itemIndex, checked) => {
|
||||
onItemCheckedRef.current?.(itemIndex, checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemChecked = undefined;
|
||||
}
|
||||
|
||||
if (onItemAllChecked) {
|
||||
gridView.onItemAllChecked = (_grid, checked) => {
|
||||
onItemAllCheckedRef.current?.(checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemAllChecked = undefined;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
|
||||
if (gridView.onItemAllChecked)
|
||||
gridView.onItemAllChecked = undefined;
|
||||
};
|
||||
}, [onItemChecked, onItemAllChecked]);
|
||||
|
||||
return (
|
||||
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
|
||||
<input
|
||||
@@ -1105,23 +1159,23 @@ const DataGrid = ({
|
||||
<span>
|
||||
진행상황:{" "}
|
||||
{statusInfo.laeSbmsnSttsCd === null ||
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
? "미작성"
|
||||
: statusInfo.laeSbmsnSttsCd === "1"
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
</span>
|
||||
<span>
|
||||
제출일:{" "}
|
||||
{statusInfo.laeSbmsnYmd
|
||||
? statusInfo.laeSbmsnYmd.length === 8 &&
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
|
||||
: statusInfo.laeSbmsnYmd
|
||||
: "미제출"}
|
||||
@@ -1159,7 +1213,7 @@ const DataGrid = ({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[65vh]">
|
||||
<div style={{ height: `${gridHeight}vh` }}>
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
@@ -1176,24 +1230,24 @@ const DataGrid = ({
|
||||
) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
const dataProvider = gridRef.current
|
||||
?.dataProvider as unknown as
|
||||
| {
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(
|
||||
@@ -1346,14 +1400,14 @@ const DataGrid = ({
|
||||
})}
|
||||
footer={
|
||||
column.footer &&
|
||||
!Array.isArray(column.footer)
|
||||
!Array.isArray(column.footer)
|
||||
? {
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
mergeRule={
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
type ApprovalRequest,
|
||||
statisticsApprovalApi,
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
useStatsVrfcRptData,
|
||||
useStatsVrfcRsltData,
|
||||
} from "@/entities/statistics";
|
||||
import type { VerifyResultItem } from "@/entities/statistics/model/general-verification-interface";
|
||||
import type { StatsInputStatusList } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
@@ -12,259 +36,364 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
useComboboxAnchor,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
statisticsVerificationDetailColumnInfo,
|
||||
statisticsVerificationSummaryColumnInfo,
|
||||
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-columns";
|
||||
import {
|
||||
APPROVAL_STATUS_OPTIONS,
|
||||
DEFAULT_FILTERS,
|
||||
DEFAULT_SUB_FILTERS,
|
||||
fetchStatisticsVerificationDetailMock,
|
||||
fetchStatisticsVerificationSummaryMock,
|
||||
getSigunguOptions,
|
||||
getYearOptions,
|
||||
SIDO_OPTIONS,
|
||||
STATS_INDICATOR_OPTIONS,
|
||||
STATUS_OPTIONS,
|
||||
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-mock";
|
||||
import type {
|
||||
StatisticsVerificationDetailRow,
|
||||
StatisticsVerificationFilterState,
|
||||
StatisticsVerificationSubFilterState,
|
||||
StatisticsVerificationSummaryRow,
|
||||
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
|
||||
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
const getYearOptions = () => {
|
||||
const currentYear = dayjs().year();
|
||||
const yearOptionsRange = Array.from(
|
||||
{ length: currentYear - 2017 },
|
||||
(_, i) => {
|
||||
const year = 2017 + i;
|
||||
return { label: String(year), value: String(year) };
|
||||
},
|
||||
);
|
||||
return yearOptionsRange.map((year) => ({
|
||||
label: year.label,
|
||||
value: year.value,
|
||||
}));
|
||||
};
|
||||
|
||||
function isSameStatsInputStatusRow(
|
||||
a: StatsInputStatusList,
|
||||
b: StatsInputStatusList,
|
||||
): boolean {
|
||||
return (
|
||||
a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd
|
||||
);
|
||||
}
|
||||
|
||||
function isSameVerifyResultRow(
|
||||
a: VerifyResultItem,
|
||||
b: VerifyResultItem,
|
||||
): boolean {
|
||||
if (a.vrfcSn != null && b.vrfcSn != null) return a.vrfcSn === b.vrfcSn;
|
||||
return (
|
||||
a.statsYr === b.statsYr &&
|
||||
a.statsCd === b.statsCd &&
|
||||
a.ctpvNm === b.ctpvNm &&
|
||||
a.sggNm === b.sggNm &&
|
||||
a.vrfcTrgtNm === b.vrfcTrgtNm &&
|
||||
a.vrfcMthdCd === b.vrfcMthdCd
|
||||
);
|
||||
}
|
||||
|
||||
function useGridCheckboxSelection<T>(
|
||||
sourceRows: T[],
|
||||
isSameRow: (a: T, b: T) => boolean,
|
||||
setSelected: Dispatch<SetStateAction<T[]>>,
|
||||
) {
|
||||
const handleItemChecked = useCallback(
|
||||
(itemIndex: number, checked: boolean) => {
|
||||
const origin = sourceRows[itemIndex];
|
||||
if (origin === undefined) return;
|
||||
setSelected((prev) => {
|
||||
if (checked) {
|
||||
if (prev.some((r) => isSameRow(r, origin))) return prev;
|
||||
return [...prev, origin];
|
||||
}
|
||||
return prev.filter((r) => !isSameRow(r, origin));
|
||||
});
|
||||
},
|
||||
[sourceRows, isSameRow, setSelected],
|
||||
);
|
||||
|
||||
const handleAllChecked = useCallback(
|
||||
(checked: boolean) => {
|
||||
if (checked) setSelected([...sourceRows]);
|
||||
else setSelected([]);
|
||||
},
|
||||
[sourceRows, setSelected],
|
||||
);
|
||||
|
||||
return { handleItemChecked, handleAllChecked };
|
||||
}
|
||||
|
||||
const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
||||
ctpvNm: null,
|
||||
sggNm: null,
|
||||
statsYr: String(dayjs().year() - 1),
|
||||
sewerBzmnCd: null,
|
||||
statsCd: null,
|
||||
};
|
||||
|
||||
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
||||
...DEFAULT_FILTERS,
|
||||
prcsYnList: "1,2,3",
|
||||
pbcpAprvYn: "N",
|
||||
};
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: "수정완료", value: "1" },
|
||||
{ label: "예외통과", value: "2" },
|
||||
{ label: "반려", value: "3" },
|
||||
];
|
||||
|
||||
const APPROVAL_STATUS_OPTIONS = [
|
||||
{ label: "승인", value: "Y" },
|
||||
{ label: "미승인", value: "N" },
|
||||
];
|
||||
|
||||
function StatisticsVerificationReport() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const yearOptions = useMemo(() => getYearOptions(), []);
|
||||
const { confirm } = useConfirm();
|
||||
const [filters, setFilters] =
|
||||
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
||||
const [subFilters, setSubFilters] =
|
||||
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
|
||||
const [summaryRows, setSummaryRows] = useState<
|
||||
StatisticsVerificationSummaryRow[]
|
||||
const [submittedMainFilters, setSubmittedMainFilters] =
|
||||
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
||||
const [submittedDetailParams, setSubmittedDetailParams] = useState<{
|
||||
main: StatisticsVerificationFilterState;
|
||||
sub: StatisticsVerificationSubFilterState;
|
||||
}>({
|
||||
main: DEFAULT_FILTERS,
|
||||
sub: DEFAULT_SUB_FILTERS,
|
||||
});
|
||||
const [selectedSummaryRows, setSelectedSummaryRows] = useState<
|
||||
StatsInputStatusList[]
|
||||
>([]);
|
||||
const [detailRows, setDetailRows] = useState<
|
||||
StatisticsVerificationDetailRow[]
|
||||
const [selectedDetailRows, setSelectedDetailRows] = useState<
|
||||
VerifyResultItem[]
|
||||
>([]);
|
||||
const [summaryGridRows, setSummaryGridRows] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
const [detailGridRows, setDetailGridRows] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
const [selectedSummaryRow, setSelectedSummaryRow] =
|
||||
useState<StatisticsVerificationSummaryRow | null>(null);
|
||||
const [selectedDetailRow, setSelectedDetailRow] =
|
||||
useState<StatisticsVerificationDetailRow | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const sigunguOptions = useMemo(
|
||||
() => getSigunguOptions(filters.sido),
|
||||
[filters.sido],
|
||||
const { data: sidoData } = useStAnlsSidoCombo();
|
||||
const { data: sigunguData } = useStAnlsSigunguCombo(
|
||||
filters.sewerBzmnCd ?? "",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void handleSearch(filters, subFilters);
|
||||
}, []);
|
||||
const statisticsVerificationReportQuery =
|
||||
useStatsVrfcRptData(submittedMainFilters);
|
||||
const statisticsVerificationResultQuery = useStatsVrfcRsltData(
|
||||
submittedDetailParams,
|
||||
);
|
||||
|
||||
async function handleSearch(
|
||||
const statisticsVerificationReportData =
|
||||
statisticsVerificationReportQuery.data ?? [];
|
||||
const statisticsVerificationReportGridRows = useMemo(
|
||||
() => toGridRows(statisticsVerificationReportData),
|
||||
[statisticsVerificationReportData],
|
||||
);
|
||||
const statisticsVerificationResultData = useMemo(
|
||||
() =>
|
||||
statisticsVerificationResultQuery.data?.pages.flatMap(
|
||||
(page) => page.verifyResultList ?? [],
|
||||
) ?? [],
|
||||
[statisticsVerificationResultQuery.data],
|
||||
);
|
||||
const statisticsVerificationResultGridRows = useMemo(
|
||||
() => toGridRows(statisticsVerificationResultData),
|
||||
[statisticsVerificationResultData],
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
statisticsVerificationReportQuery.isFetching ||
|
||||
statisticsVerificationResultQuery.isFetching;
|
||||
const isAppendingDetail =
|
||||
statisticsVerificationResultQuery.isFetchingNextPage;
|
||||
const canLoadMoreDetail = Boolean(
|
||||
statisticsVerificationResultQuery.hasNextPage,
|
||||
);
|
||||
|
||||
const {
|
||||
handleItemChecked: handleReportItemChecked,
|
||||
handleAllChecked: handleReportAllChecked,
|
||||
} = useGridCheckboxSelection(
|
||||
statisticsVerificationReportData,
|
||||
isSameStatsInputStatusRow,
|
||||
setSelectedSummaryRows,
|
||||
);
|
||||
|
||||
const {
|
||||
handleItemChecked: handleResultItemChecked,
|
||||
handleAllChecked: handleResultAllChecked,
|
||||
} = useGridCheckboxSelection(
|
||||
statisticsVerificationResultData,
|
||||
isSameVerifyResultRow,
|
||||
setSelectedDetailRows,
|
||||
);
|
||||
|
||||
const { mutateAsync: onApproveStats } = useMutationWrapper({
|
||||
mutationFn: (approvalRequests: ApprovalRequest[]) =>
|
||||
statisticsApprovalApi.approveStats(approvalRequests),
|
||||
onSuccess: (data) => {
|
||||
alert(data.message ?? "승인이 완료되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!statisticsVerificationReportQuery.isError) return;
|
||||
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
|
||||
setSelectedSummaryRows([]);
|
||||
}, [
|
||||
alert,
|
||||
statisticsVerificationReportQuery.errorUpdatedAt,
|
||||
statisticsVerificationReportQuery.isError,
|
||||
]);
|
||||
|
||||
function handleSearch(
|
||||
nextFilters: StatisticsVerificationFilterState,
|
||||
nextSubFilters: StatisticsVerificationSubFilterState,
|
||||
summaryId?: number | null,
|
||||
) {
|
||||
setSelectedSummaryRows([]);
|
||||
setSelectedDetailRows([]);
|
||||
setSubmittedMainFilters({ ...nextFilters });
|
||||
setSubmittedDetailParams({
|
||||
main: { ...nextFilters },
|
||||
sub: { ...nextSubFilters },
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLoadMoreDetail() {
|
||||
if (isLoading || isAppendingDetail || !canLoadMoreDetail) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [summaryData, detailData] = await Promise.all([
|
||||
fetchStatisticsVerificationSummaryMock(nextFilters),
|
||||
fetchStatisticsVerificationDetailMock(
|
||||
nextFilters,
|
||||
nextSubFilters,
|
||||
summaryId,
|
||||
),
|
||||
]);
|
||||
setSummaryRows(summaryData);
|
||||
setDetailRows(detailData);
|
||||
setSummaryGridRows(toGridRows(summaryData));
|
||||
setDetailGridRows(toGridRows(detailData));
|
||||
if (summaryData.length === 0)
|
||||
alert("조회된 상단 목록 데이터가 없습니다.");
|
||||
if (detailData.length === 0)
|
||||
alert("조회된 상세 목록 데이터가 없습니다.");
|
||||
await statisticsVerificationResultQuery.fetchNextPage();
|
||||
} catch {
|
||||
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
alert("추가 데이터를 불러오는 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleDetailSearchByAreaC() {
|
||||
setSelectedDetailRows([]);
|
||||
setSubmittedDetailParams({
|
||||
main: { ...filters },
|
||||
sub: { ...subFilters },
|
||||
});
|
||||
}
|
||||
|
||||
function handleFilterChange<
|
||||
K extends keyof StatisticsVerificationFilterState,
|
||||
>(key: K, value: StatisticsVerificationFilterState[K]) {
|
||||
if (key === "sido") {
|
||||
setFilters((prev) => ({ ...prev, sido: value, sigungu: "ALL" }));
|
||||
return;
|
||||
}
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
>(key: K) {
|
||||
return (
|
||||
value?: StatisticsVerificationFilterState[K],
|
||||
optionData?: { label: string; value: string },
|
||||
) => {
|
||||
if (key === "sewerBzmnCd") {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
ctpvNm: optionData?.label,
|
||||
sewerBzmnCd: optionData?.value,
|
||||
sggNm: "",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
function handleSubFilterChange<
|
||||
K extends keyof StatisticsVerificationSubFilterState,
|
||||
>(key: K, value: StatisticsVerificationSubFilterState[K]) {
|
||||
setSubFilters((prev) => ({ ...prev, [key]: value }));
|
||||
>(key: K) {
|
||||
return (value?: StatisticsVerificationSubFilterState[K]) => {
|
||||
setSubFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
setFilters(DEFAULT_FILTERS);
|
||||
setSubFilters(DEFAULT_SUB_FILTERS);
|
||||
setSelectedSummaryRow(null);
|
||||
setSelectedDetailRow(null);
|
||||
void handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
|
||||
setSelectedSummaryRows([]);
|
||||
setSelectedDetailRows([]);
|
||||
handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
|
||||
}
|
||||
|
||||
function handleApprove() {
|
||||
if (!selectedSummaryRow) {
|
||||
alert("승인할 상단 목록 행을 먼저 선택해주세요.");
|
||||
return;
|
||||
async function handleApprove() {
|
||||
const commonParam = {
|
||||
statsYr: filters.statsYr,
|
||||
sewerBzmnCd: filters.sewerBzmnCd ?? "",
|
||||
statsCd: filters.statsCd ?? "",
|
||||
mdfrId: userInfo?.userUnqId ?? "",
|
||||
};
|
||||
const approvalRequests = selectedSummaryRows.map((row) => ({
|
||||
...commonParam,
|
||||
rjctOpnnCn: row.rjctOpnnCn ?? "",
|
||||
lastCfmtnPicNm: row.laePicNm ?? "",
|
||||
lastCfmtnPicTelNo: row.laePicTelNo ?? "",
|
||||
lastCfmtnPicDeptNm: row.laeKbrdrDeptNm ?? "",
|
||||
lastCfmtnPicJbgdNm: row.laeKbrdrJbgdNm ?? "",
|
||||
}));
|
||||
try {
|
||||
if (await confirm("승인하시겠습니까?")) {
|
||||
await onApproveStats(approvalRequests);
|
||||
}
|
||||
} catch {
|
||||
alert("승인 중 오류가 발생했습니다.");
|
||||
}
|
||||
alert(
|
||||
`승인 API TBD: ${selectedSummaryRow.sido} ${selectedSummaryRow.sigungu} (${selectedSummaryRow.reportType})`,
|
||||
);
|
||||
}
|
||||
|
||||
function handleReject() {
|
||||
if (!selectedDetailRow) {
|
||||
alert("반려할 상세 항목 행을 먼저 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
alert(
|
||||
`반려 API TBD: ${selectedDetailRow.sido} ${selectedDetailRow.sigungu} ${selectedDetailRow.detailItem}`,
|
||||
);
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
if (detailRows.length === 0) {
|
||||
alert("다운로드할 상세 목록이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
"시도",
|
||||
"시군구",
|
||||
"지표",
|
||||
"세부항목",
|
||||
"검증날짜",
|
||||
"오류내역",
|
||||
"입력값",
|
||||
"비교값",
|
||||
"상태",
|
||||
"예외사유",
|
||||
"반려의견",
|
||||
"승인여부",
|
||||
];
|
||||
const rows = detailRows.map((row) =>
|
||||
[
|
||||
row.sido,
|
||||
row.sigungu,
|
||||
row.indicator,
|
||||
row.detailItem,
|
||||
row.verifyDate,
|
||||
row.errorDetail,
|
||||
String(row.inputValue),
|
||||
String(row.compareValue),
|
||||
row.status,
|
||||
row.exceptionReason,
|
||||
row.returnOpinion,
|
||||
row.approvalYn,
|
||||
].join(","),
|
||||
);
|
||||
const csv = [headers.join(","), ...rows].join("\n");
|
||||
const blob = new Blob([`\uFEFF${csv}`], {
|
||||
type: "text/csv;charset=utf-8;",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "statistics-verification-report-detail.csv";
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleSummaryRowClick(row: Record<string, string | number>) {
|
||||
const summaryId = Number(row.summaryId);
|
||||
const selected = summaryRows.find(
|
||||
(item) => item.summaryId === summaryId,
|
||||
);
|
||||
setSelectedSummaryRow(selected ?? null);
|
||||
if (!summaryId) return;
|
||||
void handleSearch(filters, subFilters, summaryId);
|
||||
}
|
||||
|
||||
function handleDetailRowClick(row: Record<string, string | number>) {
|
||||
const detailId = Number(row.detailId);
|
||||
const selected = detailRows.find((item) => item.detailId === detailId);
|
||||
setSelectedDetailRow(selected ?? null);
|
||||
}
|
||||
function handleReject() {}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="card !py-0">
|
||||
<div className="grid grid-cols-1 gap-[0.8rem] p-[1.2rem] md:grid-cols-[repeat(4,minmax(0,1fr))_auto]">
|
||||
<div className="flex justify-between p-2">
|
||||
<div className="flex items-center gap-6">
|
||||
<FilterSelect
|
||||
label="시도"
|
||||
value={filters.sido}
|
||||
onChange={(value) => handleFilterChange("sido", value)}
|
||||
options={SIDO_OPTIONS}
|
||||
value={filters.sewerBzmnCd ?? ""}
|
||||
onChange={handleFilterChange("sewerBzmnCd")}
|
||||
options={
|
||||
sidoData?.map((item) => ({
|
||||
label: item.adstNm,
|
||||
value: item.adstCd,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="시군구"
|
||||
value={filters.sigungu}
|
||||
onChange={(value) =>
|
||||
handleFilterChange("sigungu", value)
|
||||
value={filters.sggNm ?? ""}
|
||||
onChange={handleFilterChange("sggNm")}
|
||||
options={
|
||||
sigunguData?.map((item) => ({
|
||||
label: item.adstNm,
|
||||
value: item.adstCd,
|
||||
})) ?? []
|
||||
}
|
||||
options={sigunguOptions}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="년도"
|
||||
value={filters.year}
|
||||
onChange={(value) => handleFilterChange("year", value)}
|
||||
options={yearOptions}
|
||||
value={filters.statsYr}
|
||||
onChange={handleFilterChange("statsYr")}
|
||||
options={getYearOptions()}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="통계지표"
|
||||
value={filters.statsIndicator}
|
||||
onChange={(value) =>
|
||||
handleFilterChange("statsIndicator", value)
|
||||
}
|
||||
options={STATS_INDICATOR_OPTIONS}
|
||||
/>
|
||||
|
||||
<div className="flex items-end justify-end gap-[0.6rem]">
|
||||
<Button color="green" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
void handleSearch(filters, subFilters)
|
||||
}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>통계지표</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={filters.statsCd ?? ""}
|
||||
onValueChange={handleFilterChange("statsCd")}
|
||||
placeholder="선택"
|
||||
className="w-[32rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end">
|
||||
<ApproveButton approveFunc={handleApprove} />
|
||||
<div className="flex items-end justify-end gap-[0.6rem]">
|
||||
<Button color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={() => handleSearch(filters, subFilters)}>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleApprove}
|
||||
disabled={selectedSummaryRows.length === 0}
|
||||
>
|
||||
승인
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -272,78 +401,161 @@ function StatisticsVerificationReport() {
|
||||
columnInfo={statisticsVerificationSummaryColumnInfo}
|
||||
fixedColumns={3}
|
||||
isExcelImport={false}
|
||||
onRowClick={handleSummaryRowClick}
|
||||
rowData={summaryGridRows}
|
||||
isExcelExport={false}
|
||||
onItemAllChecked={handleReportAllChecked}
|
||||
onItemChecked={handleReportItemChecked}
|
||||
rowData={statisticsVerificationReportGridRows}
|
||||
rowStartNum={2}
|
||||
setRowData={setSummaryGridRows}
|
||||
setRowData={() => {}}
|
||||
showHeaderCheckbox={true}
|
||||
gridHeight={30}
|
||||
/>
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex flex-wrap items-end justify-between gap-[0.8rem]">
|
||||
<div className="grid w-full grid-cols-1 gap-[0.8rem] md:w-auto md:grid-cols-2">
|
||||
<FilterSelect
|
||||
label="상태"
|
||||
value={subFilters.status}
|
||||
onChange={(value) =>
|
||||
handleSubFilterChange("status", value)
|
||||
}
|
||||
options={STATUS_OPTIONS}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="승인상태"
|
||||
value={subFilters.approvalStatus}
|
||||
onChange={(value) =>
|
||||
handleSubFilterChange("approvalStatus", value)
|
||||
}
|
||||
options={APPROVAL_STATUS_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-[0.6rem]">
|
||||
<Button color="gray" onClick={handleDownload}>
|
||||
다운로드
|
||||
</Button>
|
||||
<ReturnButton returnFunc={handleReject} />
|
||||
</div>
|
||||
<h3 className="mt-10 px-2 text-[1.8rem] font-bold text-title">
|
||||
검증 결과
|
||||
</h3>
|
||||
<div className="mt-2 flex flex-wrap items-end justify-between p-2">
|
||||
<div className="flex items-center gap-6">
|
||||
<FilterCombobox
|
||||
label="상태"
|
||||
value={subFilters.prcsYnList}
|
||||
onChange={handleSubFilterChange("prcsYnList")}
|
||||
options={STATUS_OPTIONS}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="승인상태"
|
||||
value={subFilters.pbcpAprvYn}
|
||||
onChange={handleSubFilterChange("pbcpAprvYn")}
|
||||
options={APPROVAL_STATUS_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-[0.6rem]">
|
||||
<Button onClick={handleDetailSearchByAreaC}>조회</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={handleReject}
|
||||
disabled={selectedDetailRows.length === 0}
|
||||
>
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columnInfo={statisticsVerificationDetailColumnInfo}
|
||||
fixedColumns={4}
|
||||
isExcelImport={false}
|
||||
onRowClick={handleDetailRowClick}
|
||||
rowData={detailGridRows}
|
||||
rowStartNum={2}
|
||||
setRowData={setDetailGridRows}
|
||||
showHeaderCheckbox={true}
|
||||
/>
|
||||
<div className="relative">
|
||||
<DataGrid
|
||||
columnInfo={statisticsVerificationDetailColumnInfo}
|
||||
fixedColumns={4}
|
||||
isExcelImport={false}
|
||||
onItemAllChecked={handleResultAllChecked}
|
||||
onItemChecked={handleResultItemChecked}
|
||||
onScrollToBottom={handleLoadMoreDetail}
|
||||
rowData={statisticsVerificationResultGridRows}
|
||||
rowStartNum={2}
|
||||
setRowData={() => {}}
|
||||
showHeaderCheckbox={true}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
{(isLoading || isAppendingDetail) && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[6rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterSelectProps {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
onChange: (
|
||||
value?: string,
|
||||
optionData?: { label: string; value: string },
|
||||
) => void;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
function FilterCombobox({
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: FilterSelectProps) {
|
||||
const anchor = useComboboxAnchor();
|
||||
const items = useMemo(
|
||||
() => options.map((option) => option.value),
|
||||
[options],
|
||||
);
|
||||
const selectedValues = useMemo(() => {
|
||||
if (!value) return [];
|
||||
return value.split(",").filter(Boolean);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap">{label}</Label>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={items}
|
||||
value={selectedValues}
|
||||
onValueChange={(next) => {
|
||||
if (next == null) {
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(next) ? next : [next];
|
||||
onChange(list.length > 0 ? list.join(",") : undefined);
|
||||
}}
|
||||
>
|
||||
<ComboboxChips ref={anchor} className="max-w-[26rem]">
|
||||
<ComboboxValue>
|
||||
{(values: string[]) => (
|
||||
<>
|
||||
{values.map((v) => (
|
||||
<ComboboxChip key={v}>
|
||||
{options.find((o) => o.value === v)
|
||||
?.label ?? v}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder="선택" />
|
||||
</>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>검색 결과가 없습니다.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item: string) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{options.find((o) => o.value === item)?.label ??
|
||||
item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-[0.8rem]">
|
||||
<Label className="min-w-[4rem] text-[1.3rem]">{label}</Label>
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger className="h-[3.2rem] min-w-[14rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap">{label}</Label>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(value) =>
|
||||
onChange(
|
||||
value,
|
||||
options.find((option) => option.value === value),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
@@ -357,5 +569,4 @@ function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export { StatisticsVerificationReport };
|
||||
export default StatisticsVerificationReport;
|
||||
|
||||
@@ -18,30 +18,30 @@ export type RealGridColumnInfo = {
|
||||
mergeRule?: { criteria: "value" | string };
|
||||
sortable?: boolean;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** setColumns 메서드가 있는 GridView */
|
||||
@@ -110,21 +110,21 @@ export type DataFieldValueCallback = (
|
||||
export type ComputedFieldConfig =
|
||||
| { fields: string[]; type: "sum" | "avg" }
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
|
||||
export type ColumnConfig = {
|
||||
name: string;
|
||||
@@ -147,31 +147,31 @@ export type ColumnConfig = {
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
};
|
||||
|
||||
export type FooterUserSpan = {
|
||||
@@ -183,16 +183,16 @@ export type FooterUserSpan = {
|
||||
export type ColumnLayoutItem =
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
| {
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
|
||||
export type ColumnInfo = {
|
||||
columns: ColumnConfig[];
|
||||
@@ -202,7 +202,10 @@ export type ColumnInfo = {
|
||||
export type GridTopButtonConfig = {
|
||||
visible?: boolean;
|
||||
text: string;
|
||||
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
||||
onClick?: (helpers: {
|
||||
openDialog: (title?: string) => void;
|
||||
closeDialog: () => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type ColumnInfoList = {
|
||||
@@ -225,11 +228,18 @@ export type CellStyleCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
};
|
||||
|
||||
export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined;
|
||||
export type CellStyleCallback = (
|
||||
params: CellStyleCallbackParams,
|
||||
) => string | undefined;
|
||||
|
||||
/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */
|
||||
export type CellTooltipCallbackParams = { row: Record<string, string | number>; fieldName: string };
|
||||
export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined;
|
||||
export type CellTooltipCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
fieldName: string;
|
||||
};
|
||||
export type CellTooltipCallback = (
|
||||
params: CellTooltipCallbackParams,
|
||||
) => string | undefined;
|
||||
|
||||
export type DataGridProps = {
|
||||
columnInfo: ColumnInfo;
|
||||
@@ -252,6 +262,11 @@ export type DataGridProps = {
|
||||
current: (() => Record<string, string | number>[]) | null;
|
||||
};
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
/** CheckBar 행 체크 변경 */
|
||||
onItemChecked?: (itemIndex: number, checked: boolean) => void;
|
||||
/** CheckBar 헤더 전체 선택/해제 */
|
||||
onItemAllChecked?: (checked: boolean) => void;
|
||||
onScrollToBottom?: () => void;
|
||||
statusInfo?: SewerageSupplyRateStatusInfo | null;
|
||||
/** 강조 및 스크롤할 행의 ID */
|
||||
highlightedRowId?: string | number | null;
|
||||
@@ -263,6 +278,7 @@ export type DataGridProps = {
|
||||
compactFullWidth?: boolean;
|
||||
/** CompactDataGrid 전용: 그리드 영역 높이 Tailwind 클래스 */
|
||||
compactGridHeightClass?: string;
|
||||
gridHeight?: number;
|
||||
};
|
||||
|
||||
export type TabDataGridProps = {
|
||||
|
||||
Reference in New Issue
Block a user