409 lines
13 KiB
TypeScript
409 lines
13 KiB
TypeScript
"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 { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
|
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
|
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";
|
|
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
|
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
|
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
|
|
|
import {
|
|
INFTWT_RESULT_ROW_SELECT_MESSAGE,
|
|
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
|
|
type InftwtResultRow,
|
|
} from "./inftwtResultTypes";
|
|
|
|
const INFTWT_RESULT_PAGE_SIZE = 10;
|
|
|
|
const INFTWT_RESULT_HEADLIST: UiHeadProps[] = [
|
|
{
|
|
id: "rowNumber",
|
|
title: "순번",
|
|
sortable: true,
|
|
sortKey: "inftwtAnlsSn",
|
|
},
|
|
{ id: "ctpvNm", title: "시도" },
|
|
{ id: "sggNm", title: "시군구" },
|
|
{ id: "swtrmtFcltNm", title: "하수처리시설", sortable: true },
|
|
{ id: "aplcnYm", title: "적용연월", sortable: true },
|
|
{ id: "drtwSwtcRt", title: "오수 전환율", sortable: true },
|
|
{ id: "anlsInfo", title: "침입수 분석정보", sortable: true },
|
|
{ id: "regDt", title: "분석일자", sortable: true },
|
|
{ id: "rgtrNm", title: "분석자" },
|
|
{ id: "scsYn", title: "분석성공여부", sortable: true },
|
|
];
|
|
|
|
type InftwtResultFilters = {
|
|
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: InftwtResultFilters = {
|
|
ctpvCd: "0",
|
|
sggCd: "0",
|
|
sggName: "",
|
|
swtrmtFcltNm: "",
|
|
aplcnYmFrom: `${new Date().getFullYear()}01`,
|
|
aplcnYmTo: createDefaultAplcnYm(),
|
|
};
|
|
|
|
const postRowSelectMessage = (row: InftwtResultRow) => {
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
window.localStorage.setItem(
|
|
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
|
|
JSON.stringify({
|
|
row,
|
|
timestamp: Date.now(),
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
console.warn("침입수 분석결과 선택 동기화 저장에 실패했습니다.", error);
|
|
}
|
|
}
|
|
|
|
if (typeof window === "undefined" || !window.opener) return;
|
|
window.opener.postMessage(
|
|
{
|
|
type: INFTWT_RESULT_ROW_SELECT_MESSAGE,
|
|
row,
|
|
},
|
|
window.location.origin,
|
|
);
|
|
};
|
|
|
|
export default function InftwtResultPopupPage() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
const [filters, setFilters] = useState<InftwtResultFilters>(DEFAULT_FILTERS);
|
|
const [selectedSwagePrcsFclt, setSelectedSwagePrcsFclt] = useState("0");
|
|
const [selectedRow, setSelectedRow] = useState<InftwtResultRow | null>(null);
|
|
const [rows, setRows] = useState<InftwtResultRow[]>([]);
|
|
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: InftwtResultFilters,
|
|
nextSort: UiTableSort | undefined = sort,
|
|
) => {
|
|
const queryParams = new URLSearchParams({
|
|
page: String(page - 1),
|
|
size: String(INFTWT_RESULT_PAGE_SIZE),
|
|
scsYn: "Y",
|
|
});
|
|
queryParams.append(
|
|
"sort",
|
|
nextSort
|
|
? `${nextSort.key},${nextSort.direction}`
|
|
: "inftwtAnlsSn,desc",
|
|
);
|
|
|
|
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 : [];
|
|
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(
|
|
totalElements - ((page - 1) * INFTWT_RESULT_PAGE_SIZE + index),
|
|
0,
|
|
),
|
|
ctpvNm: ctpvOption ? ctpvOption.text : String(item?.ctpvCd ?? "-"),
|
|
sggNm: sggOption ? sggOption.text : String(item?.sggCd ?? "-"),
|
|
scsYn: String(item?.scsYn ?? "N"),
|
|
} as InftwtResultRow;
|
|
})
|
|
.filter((item: InftwtResultRow) => 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("침입수 분석결과 목록 조회 중 오류:", error);
|
|
alert("침입수 분석결과 목록을 불러오지 못했습니다.");
|
|
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?.inftwtAnlsSn) {
|
|
alert("다운로드할 분석결과를 먼저 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await fetchAllPages<any, Record<string, unknown>>({
|
|
fetchPage: (page, size) =>
|
|
getData(
|
|
`v1/inftwtAnlsRslt/ppl/${selectedRow.inftwtAnlsSn}?page=${page}&size=${size}`,
|
|
),
|
|
getItems: (page) =>
|
|
Array.isArray(page?.content) ? page.content : [],
|
|
getTotal: (page) => Number(page?.totalElements ?? 0),
|
|
});
|
|
const excelRows = response.items;
|
|
|
|
await downloadExcelWithKoreanHeaders({
|
|
data: excelRows as unknown as JSON[],
|
|
fileName: `침입수분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.inftwtAnlsSn}`,
|
|
sheetNameArray: ["침입수 분석결과"],
|
|
headerRange: "A1:D1",
|
|
headerMapping: {
|
|
inftwtAnlsSn: "침입수분석\n분석번호",
|
|
pplMngNo: "관로\n관리번호",
|
|
anlsTm: "분석시각",
|
|
predcOilq: "예측유량\n(m³/h)",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("침입수 분석결과 다운로드 중 오류:", error);
|
|
alert("침입수 분석결과 다운로드 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={windowPopupDefinitions.inftwtResultPopup.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<InftwtResultRow & Record<string, unknown>>}
|
|
headlist={INFTWT_RESULT_HEADLIST}
|
|
title="침입수 분석결과 목록"
|
|
total={total}
|
|
page={currentPage}
|
|
pageSize={INFTWT_RESULT_PAGE_SIZE}
|
|
onPageChange={(page) => void handleSearch(page)}
|
|
onRowClick={(_, index) => handleRowClick(index)}
|
|
rowId={(row) => String(row.inftwtAnlsSn)}
|
|
scrollMode="horizontal"
|
|
minTableWidth="110rem"
|
|
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>
|
|
);
|
|
}
|