sn: appendApiSearchParams() 함수 적용
This commit is contained in:
@@ -13,6 +13,7 @@ import { useAlertStore } from "@/shared/lib";
|
|||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
import { getData } from "@/components/lib/api";
|
import { getData } from "@/components/lib/api";
|
||||||
|
import { appendApiSearchParams } from "@/shared/util/util";
|
||||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||||
import useStore from "@/store/useStore";
|
import useStore from "@/store/useStore";
|
||||||
@@ -132,16 +133,13 @@ export default function InftwtResultPopupPage() {
|
|||||||
: "inftwtAnlsSn,desc",
|
: "inftwtAnlsSn,desc",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextFilters.aplcnYmFrom)
|
appendApiSearchParams(queryParams, {
|
||||||
queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom);
|
aplcnYmFrom: nextFilters.aplcnYmFrom,
|
||||||
if (nextFilters.aplcnYmTo)
|
aplcnYmTo: nextFilters.aplcnYmTo,
|
||||||
queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo);
|
ctpvCd: nextFilters.ctpvCd,
|
||||||
if (nextFilters.ctpvCd !== "0")
|
sggCd: nextFilters.sggCd,
|
||||||
queryParams.set("ctpvCd", nextFilters.ctpvCd);
|
swtrmtFcltNm: nextFilters.swtrmtFcltNm,
|
||||||
if (nextFilters.sggCd !== "0")
|
});
|
||||||
queryParams.set("sggCd", nextFilters.sggCd);
|
|
||||||
if (nextFilters.swtrmtFcltNm.trim())
|
|
||||||
queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim());
|
|
||||||
|
|
||||||
const response = await getData(`v1/inftwtAnls?${queryParams.toString()}`);
|
const response = await getData(`v1/inftwtAnls?${queryParams.toString()}`);
|
||||||
const content = Array.isArray(response?.content) ? response.content : [];
|
const content = Array.isArray(response?.content) ? response.content : [];
|
||||||
|
|||||||
+11
-13
@@ -6,6 +6,7 @@ import { Button, Input } from "@common/components";
|
|||||||
import { StandardPopupLayout } from "@common/business_components";
|
import { StandardPopupLayout } from "@common/business_components";
|
||||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||||
import { getData } from "@/components/lib/api";
|
import { getData } from "@/components/lib/api";
|
||||||
|
import { appendApiSearchParams } from "@/shared/util/util";
|
||||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||||
import useStore from "@/store/useStore";
|
import useStore from "@/store/useStore";
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
@@ -131,19 +132,16 @@ export default function PipeAnalysisDetailPopupPage() {
|
|||||||
size: String(size),
|
size: String(size),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd);
|
appendApiSearchParams(params, {
|
||||||
if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd);
|
ctpvCd: nextFilters.ctpvCd,
|
||||||
if (nextFilters.swtrmtZoneNm !== "0")
|
sggCd: nextFilters.sggCd,
|
||||||
params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm);
|
swtrmtZoneNm: nextFilters.swtrmtZoneNm,
|
||||||
if (baseElapsedYearBucket)
|
elapsedYearBucket: baseElapsedYearBucket,
|
||||||
params.set("elapsedYearBucket", baseElapsedYearBucket);
|
materialCode: nextFilters.materialCode,
|
||||||
if (nextFilters.materialCode !== "0")
|
useCd: nextFilters.useCd,
|
||||||
params.set("materialCode", nextFilters.materialCode);
|
diameterFrom: nextFilters.diameterFrom,
|
||||||
if (nextFilters.useCd !== "0") params.set("useCd", nextFilters.useCd);
|
diameterTo: nextFilters.diameterTo,
|
||||||
if (nextFilters.diameterFrom)
|
});
|
||||||
params.set("diameterFrom", nextFilters.diameterFrom);
|
|
||||||
if (nextFilters.diameterTo)
|
|
||||||
params.set("diameterTo", nextFilters.diameterTo);
|
|
||||||
if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
|
if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
|
||||||
|
|
||||||
const response = await getData(
|
const response = await getData(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { StandardPopupLayout } from "@common/business_components";
|
|||||||
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
||||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||||
import { getData } from "@/components/lib/api";
|
import { getData } from "@/components/lib/api";
|
||||||
|
import { appendApiSearchParams } from "@/shared/util/util";
|
||||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||||
import useStore from "@/store/useStore";
|
import useStore from "@/store/useStore";
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
@@ -144,10 +145,11 @@ export default function PipeAnalysisPopupPage() {
|
|||||||
analysisType: nextFilters.analysisType,
|
analysisType: nextFilters.analysisType,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd);
|
appendApiSearchParams(params, {
|
||||||
if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd);
|
ctpvCd: nextFilters.ctpvCd,
|
||||||
if (nextFilters.swtrmtZoneNm !== "0")
|
sggCd: nextFilters.sggCd,
|
||||||
params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm);
|
swtrmtZoneNm: nextFilters.swtrmtZoneNm,
|
||||||
|
});
|
||||||
if (nextFilters.scopeRegionCode)
|
if (nextFilters.scopeRegionCode)
|
||||||
params.set("scopeRegionCode", nextFilters.scopeRegionCode);
|
params.set("scopeRegionCode", nextFilters.scopeRegionCode);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
import { getData } from "@/components/lib/api";
|
import { getData } from "@/components/lib/api";
|
||||||
|
import { appendApiSearchParams } from "@/shared/util/util";
|
||||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||||
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||||
import { useAlertStore } from "@/shared/lib";
|
import { useAlertStore } from "@/shared/lib";
|
||||||
@@ -134,16 +135,13 @@ export default function RdiiResultPopupPage() {
|
|||||||
: "rainInfwtAnlsSn,desc",
|
: "rainInfwtAnlsSn,desc",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextFilters.aplcnYmFrom)
|
appendApiSearchParams(queryParams, {
|
||||||
queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom);
|
aplcnYmFrom: nextFilters.aplcnYmFrom,
|
||||||
if (nextFilters.aplcnYmTo)
|
aplcnYmTo: nextFilters.aplcnYmTo,
|
||||||
queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo);
|
ctpvCd: nextFilters.ctpvCd,
|
||||||
if (nextFilters.ctpvCd !== "0")
|
sggCd: nextFilters.sggCd,
|
||||||
queryParams.set("ctpvCd", nextFilters.ctpvCd);
|
swtrmtFcltNm: nextFilters.swtrmtFcltNm,
|
||||||
if (nextFilters.sggCd !== "0")
|
});
|
||||||
queryParams.set("sggCd", nextFilters.sggCd);
|
|
||||||
if (nextFilters.swtrmtFcltNm.trim())
|
|
||||||
queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim());
|
|
||||||
|
|
||||||
const response = await getData(`v1/rdiiAnls?${queryParams.toString()}`);
|
const response = await getData(`v1/rdiiAnls?${queryParams.toString()}`);
|
||||||
const content = Array.isArray(response?.content) ? response.content : [];
|
const content = Array.isArray(response?.content) ? response.content : [];
|
||||||
|
|||||||
@@ -69,3 +69,11 @@
|
|||||||
- 2026-08-18 목록 강수량을 소수점 한 자리로, 헤더를 `초과빈도년도`로 변경하고 값의 `년` 접미사를 제거했다.
|
- 2026-08-18 목록 강수량을 소수점 한 자리로, 헤더를 `초과빈도년도`로 변경하고 값의 `년` 접미사를 제거했다.
|
||||||
- 기존 `.next` 산출물을 재생성한 뒤 `pnpm --filter sn run build`를 최종 통과했다.
|
- 기존 `.next` 산출물을 재생성한 뒤 `pnpm --filter sn run build`를 최종 통과했다.
|
||||||
- TypeScript 정적검사는 저장소 기존 `ignoreDeprecations` 설정과 실행 중 산출물 잠금 때문에 완료하지 못했고, Next 빌드는 최적화 단계 장기 대기 후 중단했다.
|
- 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";
|
} from "@common/business_components";
|
||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
import { getBlobData, getData, getRawJson } from "@/components/lib/api";
|
import { getData, getRawJson } from "@/components/lib/api";
|
||||||
import { fileDownload } from "@/components/lib/utils/fileDownload";
|
import { appendApiSearchParams } from "@/shared/util/util";
|
||||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
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 { useAlertStore } from "@/shared/lib";
|
||||||
import useStore from "@/store/useStore";
|
import useStore from "@/store/useStore";
|
||||||
import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange";
|
import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange";
|
||||||
|
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||||
|
import { downloadFloodPredictionStatusExcel } from "../lib/fldng-predc-prst-excel";
|
||||||
|
|
||||||
type ApiRow = {
|
type ApiRow = {
|
||||||
sggCd: string;
|
sggCd: string;
|
||||||
@@ -55,6 +57,7 @@ type Filters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
const EXCEL_DOWNLOAD_PAGE_SIZE = 500;
|
||||||
const MAP_FREQUENCIES = new Set([30, 50, 80, 100]);
|
const MAP_FREQUENCIES = new Set([30, 50, 80, 100]);
|
||||||
const FREQUENCY_OPTIONS = [30, 50, 80, 100] as const;
|
const FREQUENCY_OPTIONS = [30, 50, 80, 100] as const;
|
||||||
|
|
||||||
@@ -118,7 +121,7 @@ const buildInitialFilters = (): Filters => {
|
|||||||
rainBgngTo: "",
|
rainBgngTo: "",
|
||||||
analysisFrom: toDateTimeLocal(range.analysisFrom),
|
analysisFrom: toDateTimeLocal(range.analysisFrom),
|
||||||
analysisTo: toDateTimeLocal(range.analysisTo),
|
analysisTo: toDateTimeLocal(range.analysisTo),
|
||||||
frequencyYear: "",
|
frequencyYear: "0",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,21 +144,21 @@ const formatNumber = (value: number | null, digits: number) =>
|
|||||||
: Number(value).toFixed(digits);
|
: Number(value).toFixed(digits);
|
||||||
|
|
||||||
/** 화면 조회조건을 목록·Excel API 쿼리로 변환한다. */
|
/** 화면 조회조건을 목록·Excel API 쿼리로 변환한다. */
|
||||||
const buildQuery = (filters: Filters, page?: number) => {
|
const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (page !== undefined) {
|
if (page !== undefined) {
|
||||||
params.set("page", String(page - 1));
|
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));
|
appendApiSearchParams(params, {
|
||||||
if (filters.sggCd !== "0") params.set("sggCd", filters.sggCd.slice(0, 5));
|
ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2),
|
||||||
(
|
sggCd: filters.sggCd === "0" ? "0" : filters.sggCd.slice(0, 5),
|
||||||
["rainBgngFrom", "rainBgngTo", "analysisFrom", "analysisTo"] as const
|
rainBgngFrom: apiDate(filters.rainBgngFrom),
|
||||||
).forEach((key) => {
|
rainBgngTo: apiDate(filters.rainBgngTo),
|
||||||
const value = apiDate(filters[key]);
|
analysisFrom: apiDate(filters.analysisFrom),
|
||||||
if (value) params.set(key, value);
|
analysisTo: apiDate(filters.analysisTo),
|
||||||
|
frequencyYear: filters.frequencyYear,
|
||||||
});
|
});
|
||||||
if (filters.frequencyYear) params.set("frequencyYear", filters.frequencyYear);
|
|
||||||
return params.toString();
|
return params.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +179,7 @@ const validateFilters = (filters: Filters) => {
|
|||||||
return "분석시각의 시작은 종료보다 늦을 수 없습니다.";
|
return "분석시각의 시작은 종료보다 늦을 수 없습니다.";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
filters.frequencyYear &&
|
filters.frequencyYear != "0" &&
|
||||||
!FREQUENCY_OPTIONS.includes(
|
!FREQUENCY_OPTIONS.includes(
|
||||||
Number(filters.frequencyYear) as 30 | 50 | 80 | 100,
|
Number(filters.frequencyYear) as 30 | 50 | 80 | 100,
|
||||||
)
|
)
|
||||||
@@ -389,10 +392,25 @@ export default function FldngPredcPrstPopupContent() {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const blob = await getBlobData(
|
const result = await fetchAllPages<
|
||||||
`/sewerage-service/api/v1/fldng/predc/prst/excel?${buildQuery(appliedFilters)}`,
|
{ content: ApiRow[]; totalElements: number },
|
||||||
);
|
ApiRow
|
||||||
fileDownload(blob, "침수예측현황.xlsx");
|
>({
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
|
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
|
||||||
alert("침수예측현황 Excel 다운로드에 실패했습니다.");
|
alert("침수예측현황 Excel 다운로드에 실패했습니다.");
|
||||||
@@ -519,7 +537,7 @@ export default function FldngPredcPrstPopupContent() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="">전체</option>
|
<option value="0">전체</option>
|
||||||
{FREQUENCY_OPTIONS.map((frequency) => (
|
{FREQUENCY_OPTIONS.map((frequency) => (
|
||||||
<option key={frequency} value={String(frequency)}>
|
<option key={frequency} value={String(frequency)}>
|
||||||
{frequency}년
|
{frequency}년
|
||||||
|
|||||||
Reference in New Issue
Block a user