321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
/**
|
|
* 화면명: 침수위험지도 목록
|
|
* 침수위험지도를 검색하고 목록·엑셀·지도 보기 기능을 제공한다.
|
|
*/
|
|
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Button } from "@common/components";
|
|
import { StandardPopupLayout } from "@common/business_components";
|
|
import { getRawJson } from "@/shared/api/sewerage-api";
|
|
import LoadingOverlay from "@/components/ui/loading-bar";
|
|
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
|
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
|
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
|
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import useStore from "@/entities/reference-data/model/reference-data-store";
|
|
import {
|
|
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
|
|
SBMRG_DNGR_MAP_FREQUENCY_OPTIONS,
|
|
SBMRG_DNGR_MAP_HEADLIST,
|
|
SBMRG_DNGR_MAP_LAYER_TYPE,
|
|
SBMRG_DNGR_MAP_PAGE_SIZE,
|
|
SBMRG_DNGR_MAP_SCOPE_SIZE,
|
|
buildSbmrgDngrMapListQueryString,
|
|
extractAdministrativeCodes,
|
|
toSbmrgDngrMapTableRows,
|
|
type SbmrgDngrMapApiItem,
|
|
type SbmrgDngrMapFilters,
|
|
type SbmrgDngrMapTableRow,
|
|
} from "./sbmrg-dngr-map-types";
|
|
|
|
const unwrapPayload = <T,>(response: any): T =>
|
|
(response?.data ?? response) as T;
|
|
|
|
/** 시설물 레이어의 중점관리지역 침수위험지도 피처를 조건별 목록으로 제공한다. */
|
|
export default function SbmrgDngrMapPopupPage() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
// 입력 중인 조건과 실제 조회에 적용된 조건을 분리해 페이지 이동 시 조건이 흔들리지 않게 한다.
|
|
const [filters, setFilters] = useState<SbmrgDngrMapFilters>(
|
|
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
|
|
);
|
|
const [appliedFilters, setAppliedFilters] = useState<SbmrgDngrMapFilters>(
|
|
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
|
|
);
|
|
const [items, setItems] = useState<SbmrgDngrMapApiItem[]>([]);
|
|
const [administrativeScopeItems, setAdministrativeScopeItems] = useState<
|
|
SbmrgDngrMapApiItem[] | null
|
|
>(null);
|
|
const [total, setTotal] = useState(0);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const availableCtpvCodes = useMemo(
|
|
() =>
|
|
new Set(
|
|
(administrativeScopeItems ?? []).flatMap((item) =>
|
|
extractAdministrativeCodes(item.ctpvCd, 2),
|
|
),
|
|
),
|
|
[administrativeScopeItems],
|
|
);
|
|
|
|
const availableSggCodes = useMemo(
|
|
() =>
|
|
new Set(
|
|
(administrativeScopeItems ?? []).flatMap((item) =>
|
|
extractAdministrativeCodes(item.sggCd, 5),
|
|
),
|
|
),
|
|
[administrativeScopeItems],
|
|
);
|
|
|
|
const ctpvOptions = useMemo(
|
|
() =>
|
|
administrativeScopeItems === null
|
|
? ctpv
|
|
: ctpv.filter((item) =>
|
|
availableCtpvCodes.has(String(item.val).trim().slice(0, 2)),
|
|
),
|
|
[administrativeScopeItems, availableCtpvCodes, ctpv],
|
|
);
|
|
|
|
const sggOptions = useMemo(() => {
|
|
const scopedOptions =
|
|
administrativeScopeItems === null
|
|
? sgg
|
|
: sgg.filter((item) =>
|
|
availableSggCodes.has(String(item.val).trim().slice(0, 5)),
|
|
);
|
|
return filters.ctpvCd === "0"
|
|
? []
|
|
: scopedOptions.filter((item) =>
|
|
String(item.val).startsWith(filters.ctpvCd.slice(0, 2)),
|
|
);
|
|
}, [administrativeScopeItems, availableSggCodes, filters.ctpvCd, sgg]);
|
|
|
|
const rows = useMemo(
|
|
() =>
|
|
toSbmrgDngrMapTableRows(
|
|
items,
|
|
currentPage - 1,
|
|
SBMRG_DNGR_MAP_PAGE_SIZE,
|
|
ctpv,
|
|
sgg,
|
|
total,
|
|
),
|
|
[ctpv, currentPage, items, sgg, total],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (ctpv.length === 0) void fetchCtpv();
|
|
if (sgg.length === 0) void fetchSgg();
|
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
|
|
|
/** 마지막 적용 조건으로 서버 페이지를 조회한다. */
|
|
const loadRows = useCallback(
|
|
async (page: number, nextFilters: SbmrgDngrMapFilters) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const queryString = buildSbmrgDngrMapListQueryString(
|
|
nextFilters,
|
|
page - 1,
|
|
SBMRG_DNGR_MAP_PAGE_SIZE,
|
|
);
|
|
const response = await getRawJson(
|
|
`/sewerage-service/api/v1/fldng/sbmrgDngrMap?${queryString}`,
|
|
);
|
|
const payload = unwrapPayload<{
|
|
items?: SbmrgDngrMapApiItem[];
|
|
total?: number;
|
|
}>(response);
|
|
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
setItems(items);
|
|
setTotal(Number(payload?.total ?? 0));
|
|
setCurrentPage(page);
|
|
setAppliedFilters(nextFilters);
|
|
} catch (error) {
|
|
console.error("침수위험지도 목록 조회 중 오류:", error);
|
|
setItems([]);
|
|
setTotal(0);
|
|
alert(
|
|
error instanceof Error
|
|
? error.message
|
|
: "침수위험지도 목록 조회 중 오류가 발생했습니다.",
|
|
);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[alert],
|
|
);
|
|
|
|
/** 전체 50개 그룹에서 실제 존재하는 행정구역 코드만 검색 선택지로 제한한다. */
|
|
const loadAdministrativeScope = useCallback(async () => {
|
|
try {
|
|
const queryString = buildSbmrgDngrMapListQueryString(
|
|
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
|
|
0,
|
|
SBMRG_DNGR_MAP_SCOPE_SIZE,
|
|
);
|
|
const response = await getRawJson(
|
|
`/sewerage-service/api/v1/fldng/sbmrgDngrMap?${queryString}`,
|
|
);
|
|
const payload = unwrapPayload<{ items?: SbmrgDngrMapApiItem[] }>(
|
|
response,
|
|
);
|
|
setAdministrativeScopeItems(
|
|
Array.isArray(payload?.items) ? payload.items : [],
|
|
);
|
|
} catch (error) {
|
|
console.error("침수위험지도 행정구역 범위 조회 중 오류:", error);
|
|
setAdministrativeScopeItems(null);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadRows(1, SBMRG_DNGR_MAP_DEFAULT_FILTERS);
|
|
void loadAdministrativeScope();
|
|
}, [loadAdministrativeScope, loadRows]);
|
|
|
|
/** 현재 입력 조건을 적용하고 첫 페이지부터 다시 조회한다. */
|
|
const handleSearch = () => {
|
|
void loadRows(1, filters);
|
|
};
|
|
|
|
/** 선택한 지역·빈도 그룹의 전체 fid를 부모 지도에 전달해 전체 범위 이동·강조를 실행한다. */
|
|
const handleRowClick = (row: SbmrgDngrMapTableRow) => {
|
|
if (!Array.isArray(row.fids) || row.fids.length === 0) return;
|
|
const featureIds = row.fids.map((fid) =>
|
|
String(fid).startsWith(`${SBMRG_DNGR_MAP_LAYER_TYPE}.`)
|
|
? String(fid)
|
|
: `${SBMRG_DNGR_MAP_LAYER_TYPE}.${fid}`,
|
|
);
|
|
window.opener?.postMessage(
|
|
{
|
|
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
|
|
payload: {
|
|
layerNm: SBMRG_DNGR_MAP_LAYER_TYPE,
|
|
fids: JSON.stringify(featureIds),
|
|
},
|
|
},
|
|
window.location.origin,
|
|
);
|
|
};
|
|
|
|
return (
|
|
<StandardPopupLayout title="침수위험지도 목록">
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
|
|
<PopupSearchPanel contentClassName="flex flex-wrap items-end gap-x-5 gap-y-3">
|
|
<label className="flex items-center gap-2">
|
|
<span className="whitespace-nowrap text-[1.3rem] text-label">
|
|
시도
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[18rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
|
|
value={filters.ctpvCd}
|
|
onChange={(event) =>
|
|
setFilters({
|
|
...filters,
|
|
ctpvCd: event.target.value,
|
|
sggCd: "0",
|
|
})
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{ctpvOptions.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2">
|
|
<span className="whitespace-nowrap text-[1.3rem] text-label">
|
|
시군구
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[18rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
|
|
value={filters.sggCd}
|
|
onChange={(event) =>
|
|
setFilters({ ...filters, sggCd: event.target.value })
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{sggOptions.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2">
|
|
<span className="whitespace-nowrap text-[1.3rem] text-label">
|
|
홍수수위빈도값
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[16rem] rounded-md border border-input bg-field px-3 text-[1.3rem]"
|
|
value={filters.floodWtlvFrqVl}
|
|
onChange={(event) =>
|
|
setFilters({ ...filters, floodWtlvFrqVl: event.target.value })
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{SBMRG_DNGR_MAP_FREQUENCY_OPTIONS.map((value) => (
|
|
<option key={value} value={String(value)}>
|
|
{value}년
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="ml-auto flex justify-end gap-2">
|
|
<Button type="button" color="primary" onClick={handleSearch}>
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
onClick={() => {
|
|
setFilters(SBMRG_DNGR_MAP_DEFAULT_FILTERS);
|
|
void loadRows(1, SBMRG_DNGR_MAP_DEFAULT_FILTERS);
|
|
}}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
</PopupSearchPanel>
|
|
|
|
<MapPopupDataTable
|
|
rows={rows as Array<SbmrgDngrMapTableRow & Record<string, unknown>>}
|
|
headlist={SBMRG_DNGR_MAP_HEADLIST}
|
|
title="침수위험지도 목록"
|
|
total={total}
|
|
page={currentPage}
|
|
pageSize={SBMRG_DNGR_MAP_PAGE_SIZE}
|
|
onPageChange={(page) => void loadRows(page, appliedFilters)}
|
|
onRowClick={handleRowClick}
|
|
rowId={(row, index) =>
|
|
[
|
|
row.emphsMngRgnMngNo ?? "unknown",
|
|
row.floodWtlvFrqVl ?? "unknown",
|
|
row.ctpvCd ?? "unknown",
|
|
row.sggCd ?? "unknown",
|
|
index,
|
|
].join(":")
|
|
}
|
|
/>
|
|
|
|
{/* 인수테스트 요구: 팝업 자료의 원천을 목록 아래에 표시한다. */}
|
|
<PopupFooterNote
|
|
items={["출처 : 하수도정비 중점관리지역 20개소 과업 산출물"]}
|
|
/>
|
|
</StandardPopupLayout>
|
|
);
|
|
}
|