392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
/**
|
|
* 화면명: RDII 분석결과 목록
|
|
* RDII 분석결과를 검색하고 목록·엑셀·지도 보기 기능을 제공한다.
|
|
*/
|
|
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { DateMonthPicker } from "@common/business_components/ui/date_month_picker";
|
|
import {
|
|
StandardPopupLayout,
|
|
type UiHeadProps,
|
|
type UiTableSort,
|
|
} from "@common/business_components";
|
|
import { Button } from "@common/components";
|
|
|
|
import { getData } from "@/shared/api/sewerage-api";
|
|
import { appendApiSearchParams } from "@/shared/util/util";
|
|
import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
|
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
|
import useStore from "@/entities/reference-data/model/reference-data-store";
|
|
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
|
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
|
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
|
import LoadingOverlay from "@/components/ui/loading-bar";
|
|
|
|
import {
|
|
RDII_RESULT_ROW_SELECT_MESSAGE,
|
|
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
|
|
type RdiiResultRow,
|
|
} from "./rdii-result-types";
|
|
|
|
const RDII_RESULT_PAGE_SIZE = 10;
|
|
|
|
const RDII_RESULT_HEADLIST: UiHeadProps[] = [
|
|
{
|
|
id: "rowNumber",
|
|
title: "순번",
|
|
sortable: true,
|
|
sortKey: "rainInfwtAnlsSn",
|
|
},
|
|
{ id: "ctpvNm", title: "시도" },
|
|
{ id: "sggNm", title: "시군구" },
|
|
{ id: "swtrmtFcltNm", title: "하수처리시설", sortable: true },
|
|
{ id: "aplcnYm", title: "상수사용연월", sortable: true },
|
|
{ id: "drtwSwtcRt", title: "오수 전환율", sortable: true },
|
|
{ id: "srcNm", title: "RTK 계수 출처", sortable: true },
|
|
{ id: "cntbArea", title: "RDII 기여면적", sortable: true },
|
|
{ id: "anlsInfo", title: "RDII\n분석정보", sortable: true },
|
|
{ id: "regDt", title: "분석일자", sortable: true },
|
|
{ id: "rgtrNm", title: "분석자" },
|
|
{ id: "scsYn", title: "분석성공여부", sortable: true },
|
|
];
|
|
|
|
type RdiiResultFilters = {
|
|
ctpvCd: string;
|
|
sggCd: string;
|
|
sggName: string;
|
|
swtrmtFcltNm: string;
|
|
aplcnYmFrom: string;
|
|
aplcnYmTo: string;
|
|
};
|
|
|
|
const createDefaultAplcnYm = () => {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
return `${year}${month}`;
|
|
};
|
|
|
|
const DEFAULT_FILTERS: RdiiResultFilters = {
|
|
ctpvCd: "0",
|
|
sggCd: "0",
|
|
sggName: "",
|
|
swtrmtFcltNm: "",
|
|
aplcnYmFrom: `${new Date().getFullYear()}01`,
|
|
aplcnYmTo: createDefaultAplcnYm(),
|
|
};
|
|
|
|
const postRowSelectMessage = (row: RdiiResultRow) => {
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
window.localStorage.setItem(
|
|
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
|
|
JSON.stringify({
|
|
row,
|
|
timestamp: Date.now(),
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.warn("RDII 분석결과 선택 동기화 저장에 실패했습니다.", error);
|
|
}
|
|
}
|
|
|
|
if (typeof window === "undefined" || !window.opener) return;
|
|
window.opener.postMessage(
|
|
{
|
|
type: RDII_RESULT_ROW_SELECT_MESSAGE,
|
|
row,
|
|
},
|
|
window.location.origin,
|
|
);
|
|
};
|
|
|
|
export default function RdiiResultPopupPage() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
const [filters, setFilters] = useState<RdiiResultFilters>(DEFAULT_FILTERS);
|
|
const [selectedSwagePrcsFclt, setSelectedSwagePrcsFclt] = useState("0");
|
|
const [selectedRow, setSelectedRow] = useState<RdiiResultRow | null>(null);
|
|
const [rows, setRows] = useState<RdiiResultRow[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [sort, setSort] = useState<UiTableSort>();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (ctpv.length === 0) void fetchCtpv();
|
|
if (sgg.length === 0) void fetchSgg();
|
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
|
|
|
const fetchRows = useCallback(
|
|
async (
|
|
page: number,
|
|
nextFilters: RdiiResultFilters,
|
|
nextSort: UiTableSort | undefined = sort,
|
|
) => {
|
|
const queryParams = new URLSearchParams({
|
|
page: String(page - 1),
|
|
size: String(RDII_RESULT_PAGE_SIZE),
|
|
scsYn: "Y",
|
|
});
|
|
queryParams.append(
|
|
"sort",
|
|
nextSort
|
|
? `${nextSort.key},${nextSort.direction}`
|
|
: "rainInfwtAnlsSn,desc",
|
|
);
|
|
|
|
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 : [];
|
|
const totalElements = Number(response?.totalElements ?? content.length);
|
|
|
|
const nextRows = content
|
|
.map((item: Record<string, unknown>, index: number) => {
|
|
const formattedItem = formatDisplayData(item);
|
|
const ctpvOption = ctpv.find(
|
|
(code) => code.val.trim() === String(item?.ctpvCd ?? "").trim(),
|
|
);
|
|
const sggOption = sgg.find((code) => code.val === item?.sggCd);
|
|
|
|
return {
|
|
...(formattedItem as Record<string, unknown>),
|
|
rowNumber: Math.max(
|
|
(page - 1) * RDII_RESULT_PAGE_SIZE + index + 1,
|
|
0,
|
|
),
|
|
ctpvNm: ctpvOption ? ctpvOption.text : String(item?.ctpvCd ?? "-"),
|
|
sggNm: sggOption ? sggOption.text : String(item?.sggCd ?? "-"),
|
|
scsYn: String(item?.scsYn ?? "N"),
|
|
} as RdiiResultRow;
|
|
})
|
|
.filter((item: RdiiResultRow) => item.scsYn === "Y");
|
|
|
|
setRows(nextRows);
|
|
setTotal(totalElements);
|
|
setCurrentPage(page);
|
|
},
|
|
[ctpv, sgg, sort],
|
|
);
|
|
|
|
const loadRows = useCallback(
|
|
async (
|
|
page = 1,
|
|
nextFilters = filters,
|
|
nextSort: UiTableSort | undefined = sort,
|
|
) => {
|
|
setIsLoading(true);
|
|
try {
|
|
await fetchRows(page, nextFilters, nextSort);
|
|
} catch (error) {
|
|
console.error("RDII 분석결과 목록 조회 중 오류:", error);
|
|
alert("RDII 분석결과 목록을 불러오지 못했습니다.");
|
|
setRows([]);
|
|
setTotal(0);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[alert, fetchRows, filters, sort],
|
|
);
|
|
|
|
useEffect(() => {
|
|
void loadRows(1, DEFAULT_FILTERS, undefined);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const handleSearch = async (page = 1) => {
|
|
setSelectedRow(null);
|
|
await loadRows(page, filters);
|
|
};
|
|
|
|
/** CS 검색 카드와 동일하게 조건과 선택 결과를 기본값으로 되돌린다. */
|
|
const handleReset = async () => {
|
|
setFilters(DEFAULT_FILTERS);
|
|
setSelectedSwagePrcsFclt("0");
|
|
setSelectedRow(null);
|
|
setSort(undefined);
|
|
await loadRows(1, DEFAULT_FILTERS, undefined);
|
|
};
|
|
|
|
/** 정렬 변경 시 선택 결과를 해제하고 서버 첫 페이지를 조회한다. */
|
|
const handleSortChange = (nextSort?: UiTableSort) => {
|
|
setSort(nextSort);
|
|
setSelectedRow(null);
|
|
void loadRows(1, filters, nextSort);
|
|
};
|
|
|
|
const handleRowClick = (index?: number) => {
|
|
if (typeof index !== "number") return;
|
|
const row = rows[index];
|
|
if (!row) return;
|
|
|
|
setSelectedRow(row);
|
|
postRowSelectMessage(row);
|
|
};
|
|
|
|
const handleDownload = async () => {
|
|
if (!selectedRow?.rainInfwtAnlsSn) {
|
|
alert("다운로드할 분석결과를 먼저 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
await downloadAnalysisExcel("rdii", selectedRow.rainInfwtAnlsSn, `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`);
|
|
} catch (error) {
|
|
console.error("RDII 분석결과 다운로드 중 오류:", error);
|
|
alert("RDII 분석결과 다운로드 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={windowPopupDefinitions.rdiiResultPopup.title}
|
|
bodyClassName="overflow-x-hidden"
|
|
>
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
<PopupSearchPanel contentClassName="flex flex-wrap items-center gap-x-5 gap-y-3">
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<span className="whitespace-nowrap text-[1.3rem] font-medium text-label">
|
|
상수사용기간
|
|
</span>
|
|
<DateMonthPicker
|
|
field={{
|
|
value: filters.aplcnYmFrom,
|
|
onChange: (value: string) => {
|
|
setFilters((current) => ({
|
|
...current,
|
|
aplcnYmFrom: value,
|
|
}));
|
|
},
|
|
}}
|
|
selectClass="w-[145px] !text-[1.3rem]"
|
|
startYear={1900}
|
|
/>
|
|
<span className="text-[1.1rem] font-semibold text-muted-foreground">
|
|
~
|
|
</span>
|
|
<DateMonthPicker
|
|
field={{
|
|
value: filters.aplcnYmTo,
|
|
onChange: (value: string) => {
|
|
setFilters((current) => ({
|
|
...current,
|
|
aplcnYmTo: value,
|
|
}));
|
|
},
|
|
}}
|
|
selectClass="w-[145px] !text-[1.3rem]"
|
|
startYear={1900}
|
|
/>
|
|
</div>
|
|
|
|
<div className="min-w-0 shrink-0">
|
|
<CtpvSggPrcsFcltSelectBox
|
|
compact
|
|
className="!justify-start"
|
|
selectValueClassName="!text-[1.3rem]"
|
|
sido={filters.ctpvCd}
|
|
sigungu={filters.sggCd}
|
|
sggName={filters.sggName}
|
|
onSidoChange={(value) => {
|
|
setSelectedSwagePrcsFclt("0");
|
|
setFilters((current) => ({
|
|
...current,
|
|
ctpvCd: value,
|
|
sggCd: "0",
|
|
sggName: "",
|
|
swtrmtFcltNm: "",
|
|
}));
|
|
}}
|
|
onSigunguChange={(value) => {
|
|
setSelectedSwagePrcsFclt("0");
|
|
setFilters((current) => ({
|
|
...current,
|
|
sggCd: value,
|
|
swtrmtFcltNm: "",
|
|
}));
|
|
}}
|
|
setSggName={(value) => {
|
|
setSelectedSwagePrcsFclt("0");
|
|
setFilters((current) => ({
|
|
...current,
|
|
sggName: value ?? "",
|
|
swtrmtFcltNm: "",
|
|
}));
|
|
}}
|
|
selectedSwagePrcsFclt={selectedSwagePrcsFclt}
|
|
onSwagePrcsFcltChange={setSelectedSwagePrcsFclt}
|
|
setSwtrmtFcltNm={(value) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
swtrmtFcltNm: value ?? "",
|
|
}))
|
|
}
|
|
setIsLoading={setIsLoading}
|
|
/>
|
|
</div>
|
|
|
|
<div className="ml-auto flex shrink-0 items-center justify-end gap-2">
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
onClick={() => void handleSearch(1)}
|
|
>
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
onClick={() => void handleReset()}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
</PopupSearchPanel>
|
|
|
|
<MapPopupDataTable
|
|
rows={rows as Array<RdiiResultRow & Record<string, unknown>>}
|
|
headlist={RDII_RESULT_HEADLIST}
|
|
title="RDII 분석결과 목록"
|
|
total={total}
|
|
page={currentPage}
|
|
pageSize={RDII_RESULT_PAGE_SIZE}
|
|
onPageChange={(page) => void handleSearch(page)}
|
|
onRowClick={(_, index) => handleRowClick(index)}
|
|
rowId={(row) => String(row.rainInfwtAnlsSn)}
|
|
scrollMode="horizontal"
|
|
minTableWidth="125rem"
|
|
sort={sort}
|
|
onSortChange={handleSortChange}
|
|
actions={
|
|
<Button type="button" onClick={() => void handleDownload()}>
|
|
엑셀다운로드
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{selectedRow ? (
|
|
<div className="rounded-sm border border-border bg-secondary px-4 py-3 text-[1rem] text-label">
|
|
{"선택 분석: "}
|
|
<span className="font-semibold text-foreground">
|
|
{selectedRow.anlsInfo}
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
</StandardPopupLayout>
|
|
);
|
|
}
|