250 lines
8.6 KiB
TypeScript
250 lines
8.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Button, Input } from "@common/components";
|
|
import { StandardPopupLayout } from "@common/business_components";
|
|
import { getRawJson } from "@/components/lib/api";
|
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
|
import {
|
|
AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
|
|
buildAgedPipeStatusByCtpvHeadList,
|
|
buildAgedPipeStatusYearOptions,
|
|
normalizeAgedPipeStatusByCtpvResponse,
|
|
normalizeAgedPipeStatusByCtpvFilters,
|
|
toAgedPipeStatusByCtpvRows,
|
|
validateAgedPipeStatusByCtpvFilters,
|
|
type AgedPipeStatusByCtpvFilters,
|
|
type AgedPipeStatusByCtpvResponse,
|
|
type AgedPipeStatusByCtpvRow,
|
|
} from "./agedPipeStatusByCtpvUtils";
|
|
import { downloadAgedPipeStatusByCtpvCsv } from "./agedPipeStatusByCtpvExport";
|
|
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
|
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
|
|
|
// CS 목록과 동일하게 한 페이지에는 최대 10개 시도만 표시한다.
|
|
const AGED_PIPE_STATUS_PAGE_SIZE = 10;
|
|
|
|
export default function AgedPipeStatusByCtpvPopupPage() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
// 조회조건 입력 상태다. 기본값 30년, 최근 연도 기준 단년 조회로 시작한다.
|
|
const [filters, setFilters] = useState<AgedPipeStatusByCtpvFilters>(() =>
|
|
normalizeAgedPipeStatusByCtpvFilters({
|
|
elapsedYears: AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
|
|
}),
|
|
);
|
|
// 서버 응답을 화면 그리드에 맞게 평탄화한 row 목록이다.
|
|
const [rows, setRows] = useState<AgedPipeStatusByCtpvRow[]>([]);
|
|
// 동적 컬럼 생성을 위한 기준년도 목록이다.
|
|
const [years, setYears] = useState<number[]>([]);
|
|
// 조회/다운로드 중 로딩 overlay를 제어한다.
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
// 사용자에게 현재 다운로드 상태를 보여주는 진행 문구다.
|
|
const [downloadProgressText, setDownloadProgressText] = useState("");
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
const yearOptions = useMemo(() => buildAgedPipeStatusYearOptions(), []);
|
|
const headList = useMemo(
|
|
() => [
|
|
{ id: "rowNumber", title: "순번", widthPercent: 7 },
|
|
...buildAgedPipeStatusByCtpvHeadList(years),
|
|
],
|
|
[years],
|
|
);
|
|
const pageRows = useMemo(
|
|
() =>
|
|
rows.slice(
|
|
(currentPage - 1) * AGED_PIPE_STATUS_PAGE_SIZE,
|
|
currentPage * AGED_PIPE_STATUS_PAGE_SIZE,
|
|
),
|
|
[currentPage, rows],
|
|
);
|
|
|
|
const loadRows = useCallback(
|
|
async (nextFilters: AgedPipeStatusByCtpvFilters) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const normalized = normalizeAgedPipeStatusByCtpvFilters(nextFilters);
|
|
const queryParams = new URLSearchParams({
|
|
startYear: String(normalized.startYear),
|
|
endYear: String(normalized.endYear),
|
|
elapsedYears: String(normalized.elapsedYears),
|
|
});
|
|
const rawResponse = await getRawJson(
|
|
`v1/swagePpl/aged-status-by-ctpv?${queryParams.toString()}`,
|
|
);
|
|
const response = normalizeAgedPipeStatusByCtpvResponse(
|
|
rawResponse,
|
|
) as AgedPipeStatusByCtpvResponse;
|
|
setYears(Array.isArray(response?.years) ? response.years : []);
|
|
setRows(toAgedPipeStatusByCtpvRows(response));
|
|
setCurrentPage(1);
|
|
} catch (error) {
|
|
console.error("시도별 노후관로현황 조회 중 오류:", error);
|
|
setYears([]);
|
|
setRows([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
void loadRows(filters);
|
|
}, [filters, loadRows]);
|
|
|
|
const handleSearch = async () => {
|
|
const normalized = normalizeAgedPipeStatusByCtpvFilters(filters);
|
|
if (!validateAgedPipeStatusByCtpvFilters(normalized)) {
|
|
alert(
|
|
"기준년도는 최대 3년 범위 내에서 선택하고, 경과년도는 1~100 사이로 입력해주세요.",
|
|
);
|
|
return;
|
|
}
|
|
setFilters(normalized);
|
|
await loadRows(normalized);
|
|
};
|
|
|
|
const handleDownload = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setDownloadProgressText("시도별 노후관로현황 CSV를 준비하고 있습니다.");
|
|
await downloadAgedPipeStatusByCtpvCsv(filters);
|
|
setDownloadProgressText("");
|
|
} catch (error) {
|
|
console.error("시도별 노후관로현황 다운로드 중 오류:", error);
|
|
setDownloadProgressText("");
|
|
alert(
|
|
error instanceof Error
|
|
? error.message
|
|
: "시도별 노후관로현황 CSV 다운로드 중 오류가 발생했습니다.",
|
|
);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={windowPopupDefinitions.agedPipeStatusByCtpvPopup.title}
|
|
bodyClassName="overflow-x-hidden"
|
|
>
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
|
|
{downloadProgressText ? (
|
|
<div className="rounded-sm border border-primary bg-primary-transparent px-4 py-3 text-[1.3rem] text-primary">
|
|
{downloadProgressText}
|
|
</div>
|
|
) : null}
|
|
|
|
<PopupSearchPanel contentClassName="flex flex-wrap items-end gap-x-5 gap-y-3">
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="whitespace-nowrap text-xl font-medium text-label">
|
|
기준년도 시작
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl"
|
|
value={filters.startYear}
|
|
onChange={(event) =>
|
|
setFilters((current) =>
|
|
normalizeAgedPipeStatusByCtpvFilters({
|
|
...current,
|
|
startYear: Number(event.target.value),
|
|
}),
|
|
)
|
|
}
|
|
>
|
|
{yearOptions.map((year) => (
|
|
<option key={year} value={year}>
|
|
{year}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="whitespace-nowrap text-xl font-medium text-label">
|
|
기준년도 종료
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl"
|
|
value={filters.endYear}
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
endYear: Number(event.target.value),
|
|
}))
|
|
}
|
|
>
|
|
{yearOptions.map((year) => (
|
|
<option key={year} value={year}>
|
|
{year}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="whitespace-nowrap text-xl font-medium text-label">
|
|
노후관로 기준(년)
|
|
</span>
|
|
<Input
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={String(filters.elapsedYears)}
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
elapsedYears: Number(event.target.value.replace(/[^0-9]/g, "")),
|
|
}))
|
|
}
|
|
className="w-[12rem]"
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter") void handleSearch();
|
|
}}
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex min-w-fit flex-1 items-center justify-end gap-2">
|
|
<Button onClick={handleSearch} color="primary">
|
|
조회
|
|
</Button>
|
|
<Button
|
|
color="white"
|
|
onClick={() => {
|
|
const resetFilters = normalizeAgedPipeStatusByCtpvFilters({
|
|
elapsedYears: AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
|
|
});
|
|
setFilters(resetFilters);
|
|
void loadRows(resetFilters);
|
|
}}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
</PopupSearchPanel>
|
|
|
|
<MapPopupDataTable
|
|
rows={
|
|
pageRows as Array<AgedPipeStatusByCtpvRow & Record<string, unknown>>
|
|
}
|
|
headlist={headList}
|
|
title="시도별 노후관로 목록"
|
|
total={rows.length}
|
|
page={currentPage}
|
|
pageSize={AGED_PIPE_STATUS_PAGE_SIZE}
|
|
onPageChange={setCurrentPage}
|
|
scrollMode={years.length > 2 ? "horizontal" : "none"}
|
|
minTableWidth={years.length > 2 ? "120rem" : "100%"}
|
|
actions={
|
|
<Button type="button" onClick={handleDownload}>
|
|
엑셀다운로드
|
|
</Button>
|
|
}
|
|
/>
|
|
</StandardPopupLayout>
|
|
);
|
|
}
|