sn: appendApiSearchParams() 함수 적용

This commit is contained in:
2026-09-03 17:09:58 +09:00
parent 4cde9b67c1
commit 5da89e8ee7
7 changed files with 144 additions and 56 deletions
@@ -13,6 +13,7 @@ import { useAlertStore } from "@/shared/lib";
import { Button } from "@common/components";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import useStore from "@/store/useStore";
@@ -132,16 +133,13 @@ export default function InftwtResultPopupPage() {
: "inftwtAnlsSn,desc",
);
if (nextFilters.aplcnYmFrom)
queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom);
if (nextFilters.aplcnYmTo)
queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo);
if (nextFilters.ctpvCd !== "0")
queryParams.set("ctpvCd", nextFilters.ctpvCd);
if (nextFilters.sggCd !== "0")
queryParams.set("sggCd", nextFilters.sggCd);
if (nextFilters.swtrmtFcltNm.trim())
queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim());
appendApiSearchParams(queryParams, {
aplcnYmFrom: nextFilters.aplcnYmFrom,
aplcnYmTo: nextFilters.aplcnYmTo,
ctpvCd: nextFilters.ctpvCd,
sggCd: nextFilters.sggCd,
swtrmtFcltNm: nextFilters.swtrmtFcltNm,
});
const response = await getData(`v1/inftwtAnls?${queryParams.toString()}`);
const content = Array.isArray(response?.content) ? response.content : [];
@@ -6,6 +6,7 @@ import { Button, Input } from "@common/components";
import { StandardPopupLayout } from "@common/business_components";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/store/useStore";
import { useAlertStore } from "@/shared/lib";
@@ -131,19 +132,16 @@ export default function PipeAnalysisDetailPopupPage() {
size: String(size),
});
if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd);
if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd);
if (nextFilters.swtrmtZoneNm !== "0")
params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm);
if (baseElapsedYearBucket)
params.set("elapsedYearBucket", baseElapsedYearBucket);
if (nextFilters.materialCode !== "0")
params.set("materialCode", nextFilters.materialCode);
if (nextFilters.useCd !== "0") params.set("useCd", nextFilters.useCd);
if (nextFilters.diameterFrom)
params.set("diameterFrom", nextFilters.diameterFrom);
if (nextFilters.diameterTo)
params.set("diameterTo", nextFilters.diameterTo);
appendApiSearchParams(params, {
ctpvCd: nextFilters.ctpvCd,
sggCd: nextFilters.sggCd,
swtrmtZoneNm: nextFilters.swtrmtZoneNm,
elapsedYearBucket: baseElapsedYearBucket,
materialCode: nextFilters.materialCode,
useCd: nextFilters.useCd,
diameterFrom: nextFilters.diameterFrom,
diameterTo: nextFilters.diameterTo,
});
if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
const response = await getData(
@@ -14,6 +14,7 @@ import { StandardPopupLayout } from "@common/business_components";
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import useStore from "@/store/useStore";
import { useAlertStore } from "@/shared/lib";
@@ -144,10 +145,11 @@ export default function PipeAnalysisPopupPage() {
analysisType: nextFilters.analysisType,
});
if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd);
if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd);
if (nextFilters.swtrmtZoneNm !== "0")
params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm);
appendApiSearchParams(params, {
ctpvCd: nextFilters.ctpvCd,
sggCd: nextFilters.sggCd,
swtrmtZoneNm: nextFilters.swtrmtZoneNm,
});
if (nextFilters.scopeRegionCode)
params.set("scopeRegionCode", nextFilters.scopeRegionCode);
@@ -10,6 +10,7 @@ import {
import { Button } from "@common/components";
import { getData } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { useAlertStore } from "@/shared/lib";
@@ -134,16 +135,13 @@ export default function RdiiResultPopupPage() {
: "rainInfwtAnlsSn,desc",
);
if (nextFilters.aplcnYmFrom)
queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom);
if (nextFilters.aplcnYmTo)
queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo);
if (nextFilters.ctpvCd !== "0")
queryParams.set("ctpvCd", nextFilters.ctpvCd);
if (nextFilters.sggCd !== "0")
queryParams.set("sggCd", nextFilters.sggCd);
if (nextFilters.swtrmtFcltNm.trim())
queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim());
appendApiSearchParams(queryParams, {
aplcnYmFrom: nextFilters.aplcnYmFrom,
aplcnYmTo: nextFilters.aplcnYmTo,
ctpvCd: nextFilters.ctpvCd,
sggCd: nextFilters.sggCd,
swtrmtFcltNm: nextFilters.swtrmtFcltNm,
});
const response = await getData(`v1/rdiiAnls?${queryParams.toString()}`);
const content = Array.isArray(response?.content) ? response.content : [];
@@ -69,3 +69,11 @@
- 2026-08-18 목록 강수량을 소수점 한 자리로, 헤더를 `초과빈도년도`로 변경하고 값의 `년` 접미사를 제거했다.
- 기존 `.next` 산출물을 재생성한 뒤 `pnpm --filter sn run build`를 최종 통과했다.
- TypeScript 정적검사는 저장소 기존 `ignoreDeprecations` 설정과 실행 중 산출물 잠금 때문에 완료하지 못했고, Next 빌드는 최적화 단계 장기 대기 후 중단했다.
## Excel 프론트 생성 전환 결과 (2026-09-03)
- 현재 적용 검색조건으로 `/api/v1/fldng/predc/prst`를 500건 단위 전체 페이지 조회한다.
- 공통 `fetchAllPages`로 `totalElements`와 실제 수집 건수 일치를 검증한다.
- `fldng-predc-prst-excel.ts`가 화면 열 순서의 단일 시트 `침수예측현황.xlsx`를 브라우저에서 생성한다.
- 서버 `/prst/excel` Blob 호출과 `getBlobData`, `fileDownload` 사용을 제거했다.
- 독립 TypeScript `--noEmit` 검증을 통과했다.
@@ -0,0 +1,66 @@
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
export type FloodPredictionExcelRow = {
ctpvNm: string;
sggNm: string;
analysisDateTime: string;
rainBeginningDateTime: string;
observedAccumulatedRainfall: number | null;
predictionHour: number | null;
predictedAccumulatedRainfall: number | null;
exceededFrequencyYear: number | null;
floodRiskGrade: string;
};
const HEADERS: Record<string, string> = {
rowNo: "순번",
ctpvNm: "시도",
sggNm: "시군구",
analysisDateTime: "분석시각",
rainBeginningDateTime: "강우 시작시각",
observedAccumulatedRainfall: "관측 누적강수량(mm)",
predictionHour: "예측시간",
predictedAccumulatedRainfall: "예측 누적강수량(mm)",
exceededFrequencyYear: "초과빈도(년)",
floodRiskGrade: "침수위험도",
};
/** yyyyMMddHHmm 형식의 API 시각을 Excel 표시 문자열로 변환한다. */
const formatDateTime = (value: string) =>
/^\d{12}$/.test(value)
? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)} ${value.slice(8, 10)}:${value.slice(10, 12)}`
: value || "-";
/** 적용 조회조건의 전체 침수예측현황을 브라우저에서 한 시트 XLSX로 생성한다. */
export const downloadFloodPredictionStatusExcel = async (
rows: FloodPredictionExcelRow[],
) => {
const excelRows = rows.map((row, index) => ({
rowNo: index + 1,
ctpvNm: row.ctpvNm || "-",
sggNm: row.sggNm || "-",
analysisDateTime: formatDateTime(row.analysisDateTime),
rainBeginningDateTime: formatDateTime(row.rainBeginningDateTime),
observedAccumulatedRainfall:
row.observedAccumulatedRainfall == null
? "-"
: Number(row.observedAccumulatedRainfall.toFixed(1)),
predictionHour:
row.predictionHour == null ? "-" : `${row.predictionHour}시간 후`,
predictedAccumulatedRainfall:
row.predictedAccumulatedRainfall == null
? "-"
: Number(row.predictedAccumulatedRainfall.toFixed(1)),
exceededFrequencyYear:
row.exceededFrequencyYear == null ? "-" : row.exceededFrequencyYear,
floodRiskGrade: row.floodRiskGrade || "-",
}));
await downloadExcelWithKoreanHeaders({
data: excelRows as unknown as JSON[],
fileName: "침수예측현황.xlsx",
sheetNameArray: ["침수예측현황"],
headerRange: "A1:J1",
headerMapping: HEADERS,
});
};
@@ -8,8 +8,8 @@ import {
} from "@common/business_components";
import { Button } from "@common/components";
import { getBlobData, getData, getRawJson } from "@/components/lib/api";
import { fileDownload } from "@/components/lib/utils/fileDownload";
import { getData, getRawJson } from "@/components/lib/api";
import { appendApiSearchParams } from "@/shared/util/util";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
@@ -18,6 +18,8 @@ import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { useAlertStore } from "@/shared/lib";
import useStore from "@/store/useStore";
import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import { downloadFloodPredictionStatusExcel } from "../lib/fldng-predc-prst-excel";
type ApiRow = {
sggCd: string;
@@ -55,6 +57,7 @@ type Filters = {
};
const PAGE_SIZE = 10;
const EXCEL_DOWNLOAD_PAGE_SIZE = 500;
const MAP_FREQUENCIES = new Set([30, 50, 80, 100]);
const FREQUENCY_OPTIONS = [30, 50, 80, 100] as const;
@@ -118,7 +121,7 @@ const buildInitialFilters = (): Filters => {
rainBgngTo: "",
analysisFrom: toDateTimeLocal(range.analysisFrom),
analysisTo: toDateTimeLocal(range.analysisTo),
frequencyYear: "",
frequencyYear: "0",
};
};
@@ -141,21 +144,21 @@ const formatNumber = (value: number | null, digits: number) =>
: Number(value).toFixed(digits);
/** 화면 조회조건을 목록·Excel API 쿼리로 변환한다. */
const buildQuery = (filters: Filters, page?: number) => {
const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => {
const params = new URLSearchParams();
if (page !== undefined) {
params.set("page", String(page - 1));
params.set("size", String(PAGE_SIZE));
params.set("size", String(size));
}
if (filters.ctpvCd !== "0") params.set("ctpvCd", filters.ctpvCd.slice(0, 2));
if (filters.sggCd !== "0") params.set("sggCd", filters.sggCd.slice(0, 5));
(
["rainBgngFrom", "rainBgngTo", "analysisFrom", "analysisTo"] as const
).forEach((key) => {
const value = apiDate(filters[key]);
if (value) params.set(key, value);
appendApiSearchParams(params, {
ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2),
sggCd: filters.sggCd === "0" ? "0" : filters.sggCd.slice(0, 5),
rainBgngFrom: apiDate(filters.rainBgngFrom),
rainBgngTo: apiDate(filters.rainBgngTo),
analysisFrom: apiDate(filters.analysisFrom),
analysisTo: apiDate(filters.analysisTo),
frequencyYear: filters.frequencyYear,
});
if (filters.frequencyYear) params.set("frequencyYear", filters.frequencyYear);
return params.toString();
};
@@ -176,7 +179,7 @@ const validateFilters = (filters: Filters) => {
return "분석시각의 시작은 종료보다 늦을 수 없습니다.";
}
if (
filters.frequencyYear &&
filters.frequencyYear != "0" &&
!FREQUENCY_OPTIONS.includes(
Number(filters.frequencyYear) as 30 | 50 | 80 | 100,
)
@@ -389,10 +392,25 @@ export default function FldngPredcPrstPopupContent() {
}
setLoading(true);
try {
const blob = await getBlobData(
`/sewerage-service/api/v1/fldng/predc/prst/excel?${buildQuery(appliedFilters)}`,
);
fileDownload(blob, "침수예측현황.xlsx");
const result = await fetchAllPages<
{ content: ApiRow[]; totalElements: number },
ApiRow
>({
batchSize: EXCEL_DOWNLOAD_PAGE_SIZE,
fetchPage: async (pageIndex, size) => {
const response = await getRawJson(
`/sewerage-service/api/v1/fldng/predc/prst?${buildQuery(appliedFilters, pageIndex + 1, size)}`,
);
const payload = response?.data ?? response;
return {
content: Array.isArray(payload?.content) ? payload.content : [],
totalElements: Number(payload?.totalElements ?? 0),
};
},
getItems: (response) => response.content,
getTotal: (response) => response.totalElements,
});
await downloadFloodPredictionStatusExcel(result.items);
} catch (error) {
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
alert("침수예측현황 Excel 다운로드에 실패했습니다.");
@@ -519,7 +537,7 @@ export default function FldngPredcPrstPopupContent() {
}))
}
>
<option value="">전체</option>
<option value="0">전체</option>
{FREQUENCY_OPTIONS.map((frequency) => (
<option key={frequency} value={String(frequency)}>
{frequency}년