Files
frontend/apps/sn/app/(map-layout)/map/components/sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx
T

303 lines
10 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 "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { useAlertStore } from "@/shared/lib";
import useStore from "@/store/useStore";
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 "./sbmrgDngrMapTypes";
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
: 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/sbmrg-dngr-map?${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/sbmrg-dngr-map?${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) => String(row.emphsMngRgnMngNo ?? index)}
/>
</StandardPopupLayout>
);
}