Files
frontend/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx
T

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>
);
}