Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic
This commit is contained in:
@@ -478,6 +478,60 @@ export const SwtrmtPrcsZoneExcelCol = {
|
||||
'swtrmtFcltNm': '하수처리시설명'
|
||||
};
|
||||
|
||||
// 침수흔적도
|
||||
export const FldngWtlvMapCol = [
|
||||
{ id: 'fldng_yr', title: '침수년도' },
|
||||
{ id: 'fldng_dstr_nm', title: '침수재해명' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
{ id: 'bgng_ymd', title: '침수시작일자' },
|
||||
{ id: 'fldng_bgng_hm', title: '침수시작시분' },
|
||||
{ id: 'end_ymd', title: '침수종료일자' },
|
||||
{ id: 'fldng_end_hm', title: '침수종료시분' },
|
||||
{ id: 'fldng_hr', title: '침수시간' },
|
||||
{ id: 'fldng_dstrct_nm', title: '침수지구명' },
|
||||
{ id: 'fldng_cs_dtl_cd', title: '침수원인상세코드' },
|
||||
{ id: 'avg_fldng_wtlv', title: '평균침수위' },
|
||||
{ id: 'fldng_area', title: '침수면적' },
|
||||
{ id: 'exmn_yr', title: '조사년도' },
|
||||
{ id: 'dam_amt', title: '피해금액' },
|
||||
];
|
||||
export const FldngWtlvMapCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
{ id: 'fldngYr', title: '침수년도' },
|
||||
{ id: 'fldngDstrNm', title: '침수재해명' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: 'bgngYmd', title: '침수시작일자' },
|
||||
{ id: 'fldngBgngHm', title: '침수시작시분' },
|
||||
{ id: 'endYmd', title: '침수종료일자' },
|
||||
{ id: 'fldngEndHm', title: '침수종료시분' },
|
||||
{ id: 'fldngHr', title: '침수시간' },
|
||||
{ id: 'fldngDstrctNm', title: '침수지구명' },
|
||||
{ id: 'fldngCsDtlCd', title: '침수원인상세코드' },
|
||||
{ id: 'avgFldngWtlv', title: '평균침수위' },
|
||||
{ id: 'fldngArea', title: '침수면적' },
|
||||
{ id: 'exmnYr', title: '조사년도' },
|
||||
{ id: 'damAmt', title: '피해금액' },
|
||||
];
|
||||
export const FldngWtlvMapExcelCol = {
|
||||
'fldngYr': '침수년도',
|
||||
'fldngDstrNm': '침수재해명',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'bgngYmd': '침수시작일자',
|
||||
'fldngBgngHm': '침수시작시분',
|
||||
'endYmd': '침수종료일자',
|
||||
'fldngEndHm': '침수종료시분',
|
||||
'fldngHr': '침수시간',
|
||||
'fldngDstrctNm': '침수지구명',
|
||||
'fldngCsDtlCd': '침수원인상세코드',
|
||||
'avgFldngWtlv': '평균침수위',
|
||||
'fldngArea': '침수면적',
|
||||
'exmnYr': '조사년도',
|
||||
'damAmt': '피해금액'
|
||||
};
|
||||
|
||||
// 하수맨홀
|
||||
export const MsSwageMhlCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
@@ -819,7 +873,8 @@ export const dateFields = [
|
||||
'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd',
|
||||
'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt',
|
||||
'rainBeginDt', 'rainEndDt',
|
||||
'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr'
|
||||
'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr',
|
||||
'bgng_ymd', 'end_ymd', 'bgngYmd', 'endYmd'
|
||||
];
|
||||
|
||||
// 🎯 연월 필드들 (yyyymm -> yyyy년 m월)
|
||||
@@ -848,5 +903,7 @@ export const numberFields = [
|
||||
'precdngDry', 'rainFall', 'cntncTime', 'rainIn',
|
||||
'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc',
|
||||
'bizAmt', 'count',
|
||||
'chmbr_xcnts', 'chmbr_ydnts'
|
||||
'chmbr_xcnts', 'chmbr_ydnts',
|
||||
'fldng_hr', 'avg_fldng_wtlv', 'fldng_area', 'dam_amt',
|
||||
'fldngHr', 'avgFldngWtlv', 'fldngArea', 'damAmt'
|
||||
];
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { formatFloodArea, formatFloodDepth, type FloodHistoryItem } from "./floodHistoryTypes";
|
||||
import { formatFloodArea, formatFloodDepth, type FldngHstryItem } from "./fldngHstryTypes";
|
||||
|
||||
type Props = {
|
||||
detail: FloodHistoryItem;
|
||||
detail: FldngHstryItem;
|
||||
onClose: () => void;
|
||||
position: {
|
||||
left: number;
|
||||
@@ -35,7 +35,7 @@ const renderRows = (rows: Array<[string, string]>) => (
|
||||
</table>
|
||||
);
|
||||
|
||||
export default function FloodHistoryDetailPanel({ detail, onClose, position }: Props) {
|
||||
export default function FldngHstryDetailPanel({ detail, onClose, position }: Props) {
|
||||
// 지도에서 침수흔적도 polygon을 클릭했을 때, 상세정보는 업무 의미 기준으로 3개 탭에 나눠 보여준다.
|
||||
const [activeTab, setActiveTab] = useState<DetailTab>("year");
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* 침수발생이력 popup
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import type { UiTableSort } from "@common/business_components/ui";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import {
|
||||
FLDNG_HSTRY_DEFAULT_FILTERS,
|
||||
FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
|
||||
FLDNG_HSTRY_DOWNLOAD_HEADERS,
|
||||
FLDNG_HSTRY_LIST_HEADLIST,
|
||||
FLDNG_HSTRY_PAGE_SIZE,
|
||||
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
attachFldngHstryGeometry,
|
||||
buildFldngHstryListQueryString,
|
||||
buildFldngHstryYearOptions,
|
||||
normalizeFldngHstryFilters,
|
||||
toFldngHstryTableRows,
|
||||
type FldngHstryApiItem,
|
||||
type FldngHstryFilters,
|
||||
type FldngHstryTableRow,
|
||||
} from "./fldngHstryTypes";
|
||||
|
||||
const unwrapPayload = <T,>(response: any): T =>
|
||||
(response?.data ?? response) as T;
|
||||
|
||||
const resolveOptionText = (
|
||||
code: string,
|
||||
options: Array<{ text: string; val: string | number }>,
|
||||
prefixLength?: number,
|
||||
) => {
|
||||
const normalized = String(code ?? "").trim();
|
||||
if (!normalized || normalized === "0") return "";
|
||||
const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized;
|
||||
return options.find((item) => String(item.val).trim() === lookup)?.text ?? "";
|
||||
};
|
||||
|
||||
const resolveFldngHstryApiSort = (sort?: UiTableSort) => {
|
||||
if (!sort?.key || !sort.direction) return undefined;
|
||||
return { key: sort.key, direction: sort.direction } as UiTableSort;
|
||||
};
|
||||
|
||||
const buildDownloadRows = (rows: FldngHstryTableRow[]) =>
|
||||
rows.map((row) => ({
|
||||
rowNo: row.rowNo,
|
||||
floodYearDisplay: row.floodYearDisplay,
|
||||
ctpvNm: row.ctpvNm,
|
||||
sggNm: row.sggNm,
|
||||
disasterName: row.disasterName,
|
||||
rainBgngTmDisplay: row.rainBgngTmDisplay,
|
||||
rainEndTmDisplay: row.rainEndTmDisplay,
|
||||
rainTotDisplay: row.rainTotDisplay,
|
||||
rainStrDisplay: row.rainStrDisplay,
|
||||
floodHourDisplay: row.floodHourDisplay,
|
||||
districtName: row.districtName,
|
||||
causeDetailCode: row.causeDetailCode,
|
||||
causeDetailName: row.causeDetailName,
|
||||
avgFloodDepthDisplay: row.avgFloodDepthDisplay,
|
||||
floodAreaDisplay: row.floodAreaDisplay,
|
||||
exmnYearDisplay: row.exmnYearDisplay,
|
||||
damageAmountDisplay: row.damageAmountDisplay,
|
||||
}));
|
||||
|
||||
const resolveRegionNames = (
|
||||
row: FldngHstryTableRow,
|
||||
ctpvOptions: Array<{ text: string; val: string | number }>,
|
||||
sggOptions: Array<{ text: string; val: string | number }>,
|
||||
): FldngHstryTableRow => ({
|
||||
...row,
|
||||
ctpvNm: resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) || row.ctpvNm,
|
||||
sggNm: resolveOptionText(row.sggCd ?? "", sggOptions, 5) || row.sggNm,
|
||||
});
|
||||
|
||||
export default function FldngHstryPopupPage() {
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [filters, setFilters] = useState<FldngHstryFilters>(FLDNG_HSTRY_DEFAULT_FILTERS);
|
||||
const [appliedFilters, setAppliedFilters] = useState<FldngHstryFilters>(FLDNG_HSTRY_DEFAULT_FILTERS);
|
||||
const [rows, setRows] = useState<FldngHstryTableRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [sort, setSort] = useState<UiTableSort>();
|
||||
const [appliedSort, setAppliedSort] = useState<UiTableSort>();
|
||||
|
||||
useEffect(() => {
|
||||
if (ctpv.length === 0) void fetchCtpv();
|
||||
if (sgg.length === 0) void fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rows.length === 0) return;
|
||||
if (ctpv.length === 0 && sgg.length === 0) return;
|
||||
|
||||
setRows((current) =>
|
||||
current.map((row) => resolveRegionNames(row, ctpv as any, sgg as any)),
|
||||
);
|
||||
}, [ctpv, rows.length, sgg]);
|
||||
|
||||
const fldngYearOptions = useMemo(() => buildFldngHstryYearOptions(), []);
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
|
||||
[filters.ctpvCd, sgg],
|
||||
);
|
||||
|
||||
const fetchRows = useCallback(
|
||||
async (page: number, nextFilters: FldngHstryFilters, size: number, nextSort?: UiTableSort) => {
|
||||
const normalized = normalizeFldngHstryFilters(nextFilters);
|
||||
const queryString = buildFldngHstryListQueryString(
|
||||
normalized,
|
||||
page,
|
||||
size,
|
||||
resolveFldngHstryApiSort(nextSort),
|
||||
);
|
||||
const response = await getRawJson(`/sewerage-service/api/v1/fldng/hstry?${queryString}`);
|
||||
const payload = unwrapPayload<{ items?: FldngHstryApiItem[]; total?: number }>(response);
|
||||
const items = Array.isArray(payload?.items) ? payload.items : [];
|
||||
|
||||
const displayItems = items.map((item) => {
|
||||
const hydrated = attachFldngHstryGeometry(item);
|
||||
return {
|
||||
...hydrated,
|
||||
ctpvNm: resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2),
|
||||
sggNm: resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
rows: toFldngHstryTableRows(displayItems, page, size),
|
||||
total: Number(payload?.total ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadRows = useCallback(
|
||||
async (page: number, nextFilters: FldngHstryFilters, nextSort?: UiTableSort) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const normalized = normalizeFldngHstryFilters(nextFilters);
|
||||
const response = await fetchRows(page, normalized, FLDNG_HSTRY_PAGE_SIZE, nextSort);
|
||||
setRows(response.rows);
|
||||
setTotal(response.total);
|
||||
setCurrentPage(page);
|
||||
setAppliedFilters(normalized);
|
||||
setSort(nextSort);
|
||||
setAppliedSort(nextSort);
|
||||
setSelectedIndex(-1);
|
||||
} catch (error) {
|
||||
console.error("침수발생이력 목록 조회 중 오류:", error);
|
||||
setRows([]);
|
||||
setTotal(0);
|
||||
alert(error instanceof Error ? error.message : "침수발생이력 목록 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchRows],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRows(1, FLDNG_HSTRY_DEFAULT_FILTERS, undefined);
|
||||
// 최초 로딩은 한 번만 수행한다.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSearch = async (page = 1) => {
|
||||
const normalized = normalizeFldngHstryFilters(filters);
|
||||
|
||||
if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") {
|
||||
alert("시도를 먼저 선택하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFilters(normalized);
|
||||
await loadRows(page, normalized, sort);
|
||||
};
|
||||
|
||||
const handlePageChange = async (page: number) => {
|
||||
await loadRows(page, appliedFilters, appliedSort);
|
||||
};
|
||||
|
||||
const handleSortChange = async (nextSort?: UiTableSort) => {
|
||||
await loadRows(1, appliedFilters, nextSort);
|
||||
};
|
||||
|
||||
const handleRowClick = (index?: number) => {
|
||||
if (typeof index !== "number") return;
|
||||
const row = rows[index];
|
||||
if (!row) return;
|
||||
|
||||
setSelectedIndex(index);
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
openDetail: false,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (total === 0) {
|
||||
alert("다운로드할 침수발생이력 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetchRows(
|
||||
1,
|
||||
appliedFilters,
|
||||
Math.max(total, FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE),
|
||||
appliedSort,
|
||||
);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: buildDownloadRows(response.rows) as unknown as JSON[],
|
||||
fileName: `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`,
|
||||
sheetNameArray: ["침수발생이력"],
|
||||
headerRange: "A1:Q1",
|
||||
headerMapping: FLDNG_HSTRY_DOWNLOAD_HEADERS,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("침수발생이력 다운로드 중 오류:", error);
|
||||
alert(error instanceof Error ? error.message : "침수발생이력 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="sn-popup-page w-full max-w-full overflow-x-hidden overflow-y-hidden">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">침수발생이력</h1>
|
||||
<div />
|
||||
</header>
|
||||
|
||||
<section className="flex min-h-0 flex-1 flex-col overflow-x-hidden px-4 pb-3 pt-3">
|
||||
<div className="sn-popup-filter-panel-compact grid grid-cols-1 gap-3 md:grid-cols-[180px_180px_180px_minmax(0,1fr)_auto] md:items-end">
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시도</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시군구</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
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="sn-popup-field">
|
||||
<div className="sn-popup-field-label">침수발생년도</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.floodYear}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
floodYear: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{fldngYearOptions.map((year) => (
|
||||
<option key={year} value={year}>
|
||||
{year}년
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">침수재해명</div>
|
||||
<Input
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={filters.disasterName}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
disasterName: event.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") void handleSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="sn-popup-actions md:pr-3">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="sn-popup-primary-button !text-[1.1rem]"
|
||||
onClick={() => void handleSearch(1)}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="sn-popup-primary-button !text-[1.1rem]"
|
||||
onClick={() => void handleDownload()}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto pt-2">
|
||||
<UiTable<FldngHstryTableRow>
|
||||
headName={<>침수발생이력 조회 결과 총 {total.toLocaleString()}건</>}
|
||||
tableData={rows}
|
||||
headlist={FLDNG_HSTRY_LIST_HEADLIST}
|
||||
pageSize={FLDNG_HSTRY_PAGE_SIZE}
|
||||
total={total}
|
||||
cellClick={(index) => handleRowClick(index)}
|
||||
onPageChange={(page) => void handlePageChange(page)}
|
||||
curPage={currentPage}
|
||||
index={selectedIndex}
|
||||
widthFixed={true}
|
||||
isHideCount={true}
|
||||
sort={sort}
|
||||
onSortChange={(nextSort) => void handleSortChange(nextSort)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sn-popup-help-text">
|
||||
행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
"use client";
|
||||
|
||||
import type { UiHeadProps, UiTableSort } from "@common/business_components/ui";
|
||||
|
||||
export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup";
|
||||
export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT";
|
||||
export const FLOOD_HISTORY_LAYER_TYPE = "tb_ms_fldng_wtlv_map";
|
||||
export const FLOOD_HISTORY_PREVIEW_LAYER_TYPE = "tb_sn_flood_trace_mock";
|
||||
export const FLOOD_HISTORY_LAYER_NAME = "침수흔적도";
|
||||
export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
|
||||
export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
|
||||
export const FLDNG_HSTRY_PAGE_SIZE = 10;
|
||||
export const FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE = 2000;
|
||||
|
||||
export type FldngHstryFilters = {
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
floodYear: string;
|
||||
disasterName: string;
|
||||
};
|
||||
|
||||
export type FldngHstryDepthZone = {
|
||||
zoneId: string;
|
||||
depthLabel: string;
|
||||
color: string;
|
||||
ring: [number, number][];
|
||||
};
|
||||
|
||||
export type FldngHstryApiItem = {
|
||||
fid: string;
|
||||
fldngMapFid?: string;
|
||||
fldngWtlvMapSn?: number | null;
|
||||
taskClCd?: string;
|
||||
fldngDstrNm?: string;
|
||||
exmnYr?: string;
|
||||
avgFldngWtlv?: number | null;
|
||||
fldngArea?: number | null;
|
||||
dtlOcrnCsCn?: string;
|
||||
stdgCd?: string;
|
||||
ctpvCd?: string;
|
||||
sggCd?: string;
|
||||
dataSn?: number | null;
|
||||
bgngYmd?: string;
|
||||
endYmd?: string;
|
||||
fldngBgngHm?: string;
|
||||
fldngEndHm?: string;
|
||||
fldngCsDtlCd?: string;
|
||||
fldngDstrctNm?: string;
|
||||
damAmt?: number | null;
|
||||
fldngHr?: number | null;
|
||||
fldngYr?: string;
|
||||
cstrnFbctnYn?: string;
|
||||
atchFileSn?: number | null;
|
||||
confArea?: number | null;
|
||||
confLen?: number | null;
|
||||
rainBgngTm?: string | null;
|
||||
rainEndTm?: string | null;
|
||||
rainTot?: number | null;
|
||||
rainStr?: number | null;
|
||||
};
|
||||
|
||||
export type FldngHstryItem = FldngHstryApiItem & {
|
||||
historyId: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
floodYear: string;
|
||||
exmnYear: string;
|
||||
disasterName: string;
|
||||
districtName: string;
|
||||
floodAreaHa: number;
|
||||
avgFloodDepthM: number;
|
||||
floodDepthM: number;
|
||||
gradeLabel: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
floodHour: string;
|
||||
causeDetailCode: string;
|
||||
causeDetailName: string;
|
||||
damageAmount: string;
|
||||
rainStartDateTime: string;
|
||||
rainEndDateTime: string;
|
||||
rainStartTime: string;
|
||||
rainEndTime: string;
|
||||
totalRainfallMm: number;
|
||||
rainfallIntensityMmHr: number;
|
||||
areaRing: [number, number][];
|
||||
depthZones: FldngHstryDepthZone[];
|
||||
};
|
||||
|
||||
export type FldngHstryTableRow = FldngHstryItem & {
|
||||
rowNo: number;
|
||||
floodYearDisplay: string;
|
||||
floodHourDisplay: string;
|
||||
avgFloodDepthDisplay: string;
|
||||
floodAreaDisplay: string;
|
||||
exmnYearDisplay: string;
|
||||
damageAmountDisplay: string;
|
||||
rainBgngTmDisplay: string;
|
||||
rainEndTmDisplay: string;
|
||||
rainTotDisplay: string;
|
||||
rainStrDisplay: string;
|
||||
};
|
||||
|
||||
export const FLDNG_HSTRY_DEFAULT_FILTERS: FldngHstryFilters = {
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
floodYear: "",
|
||||
disasterName: "",
|
||||
};
|
||||
|
||||
export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
|
||||
{ color: "#fff27a", label: "0.0~0.5m" },
|
||||
{ color: "#c2f13d", label: "0.5~1.0m" },
|
||||
{ color: "#67d8ff", label: "1.0~2.0m" },
|
||||
{ color: "#8f8bff", label: "2.0~5.0m" },
|
||||
{ color: "#f2528b", label: "5.0m 이상" },
|
||||
] as const;
|
||||
|
||||
export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "NO", widthPercent: 5, sortable: false },
|
||||
{ id: "floodYearDisplay", title: "침수년도", widthPercent: 6, sortable: true, sortKey: "fldngYr" },
|
||||
{ id: "ctpvNm", title: "시도", widthPercent: 7, sortable: true, sortKey: "ctpvCd" },
|
||||
{ id: "sggNm", title: "시군구", widthPercent: 8, sortable: true, sortKey: "sggCd" },
|
||||
{ id: "disasterName", title: "침수재해명", widthPercent: 14, sortable: true, sortKey: "fldngDstrNm" },
|
||||
{ id: "rainBgngTmDisplay", title: "강우시작일시", widthPercent: 10, sortable: true, sortKey: "rainBgngTm" },
|
||||
{ id: "rainEndTmDisplay", title: "강우종료일시", widthPercent: 10, sortable: true, sortKey: "rainEndTm" },
|
||||
{ id: "rainTotDisplay", title: "총강우량(mm)", widthPercent: 8, sortable: true, sortKey: "rainTot" },
|
||||
{ id: "rainStrDisplay", title: "강우강도(mm/hr)", widthPercent: 8, sortable: true, sortKey: "rainStr" },
|
||||
{ id: "floodHourDisplay", title: "침수시간", widthPercent: 7, sortable: true, sortKey: "fldngHr" },
|
||||
{ id: "districtName", title: "침수지구명", widthPercent: 10, sortable: true, sortKey: "fldngDstrctNm" },
|
||||
{ id: "causeDetailCode", title: "침수원인상세코드", widthPercent: 8, sortable: true, sortKey: "fldngCsDtlCd" },
|
||||
{ id: "causeDetailName", title: "침수원인상세", widthPercent: 12, sortable: false },
|
||||
{ id: "avgFloodDepthDisplay", title: "평균침수위", widthPercent: 7, sortable: true, sortKey: "avgFldngWtlv" },
|
||||
{ id: "floodAreaDisplay", title: "침수면적", widthPercent: 7, sortable: true, sortKey: "fldngArea" },
|
||||
{ id: "exmnYearDisplay", title: "조사년도", widthPercent: 6, sortable: true, sortKey: "exmnYr" },
|
||||
{ id: "damageAmountDisplay", title: "피해금액", widthPercent: 9, sortable: true, sortKey: "damAmt" },
|
||||
];
|
||||
|
||||
export const FLDNG_HSTRY_DOWNLOAD_HEADERS: Record<string, string> = {
|
||||
rowNo: "NO",
|
||||
floodYearDisplay: "침수년도",
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
disasterName: "침수재해명",
|
||||
rainBgngTmDisplay: "강우시작일시",
|
||||
rainEndTmDisplay: "강우종료일시",
|
||||
rainTotDisplay: "총강우량(mm)",
|
||||
rainStrDisplay: "강우강도(mm/hr)",
|
||||
floodHourDisplay: "침수시간",
|
||||
districtName: "침수지구명",
|
||||
causeDetailCode: "침수원인상세코드",
|
||||
causeDetailName: "침수원인상세",
|
||||
avgFloodDepthDisplay: "평균침수위",
|
||||
floodAreaDisplay: "침수면적",
|
||||
exmnYearDisplay: "조사년도",
|
||||
damageAmountDisplay: "피해금액",
|
||||
};
|
||||
|
||||
const toRadian = (degree: number) => (degree * Math.PI) / 180;
|
||||
|
||||
const createIrregularRing = ({
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
scales,
|
||||
rotationDeg = 0,
|
||||
}: {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
scales: number[];
|
||||
rotationDeg?: number;
|
||||
}): [number, number][] => {
|
||||
const rotation = toRadian(rotationDeg);
|
||||
const points = scales.map((scale, index) => {
|
||||
const angle = rotation + (Math.PI * 2 * index) / scales.length;
|
||||
const jitterX = radiusX * scale * Math.cos(angle);
|
||||
const jitterY = radiusY * scale * Math.sin(angle);
|
||||
return [centerX + jitterX, centerY + jitterY] as [number, number];
|
||||
});
|
||||
|
||||
return [...points, points[0]];
|
||||
};
|
||||
|
||||
const toNumericSeed = (value: string) =>
|
||||
Array.from(value).reduce((sum, char, index) => sum + char.charCodeAt(0) * (index + 1), 0);
|
||||
|
||||
const buildGeometrySeed = (fid: string) => {
|
||||
const seed = toNumericSeed(fid);
|
||||
return {
|
||||
centerX: 1050000 + (seed % 120000),
|
||||
centerY: 1680000 + (seed % 150000),
|
||||
width: 14000 + (seed % 9000),
|
||||
height: 11000 + (seed % 7000),
|
||||
};
|
||||
};
|
||||
|
||||
const createFldngHstryDepthZones = (
|
||||
historyId: string,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): FldngHstryDepthZone[] => {
|
||||
return [
|
||||
{
|
||||
zoneId: `${historyId}-depth-1`,
|
||||
depthLabel: "0.0~0.5m",
|
||||
color: "#fff27a",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX - 2600,
|
||||
centerY: centerY - 1200,
|
||||
radiusX: 2900,
|
||||
radiusY: 2300,
|
||||
rotationDeg: -18,
|
||||
scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-2`,
|
||||
depthLabel: "0.5~1.0m",
|
||||
color: "#c2f13d",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 1800,
|
||||
centerY: centerY - 600,
|
||||
radiusX: 3500,
|
||||
radiusY: 2800,
|
||||
rotationDeg: 12,
|
||||
scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-3`,
|
||||
depthLabel: "1.0~2.0m",
|
||||
color: "#67d8ff",
|
||||
ring: createIrregularRing({
|
||||
centerX,
|
||||
centerY: centerY + 1800,
|
||||
radiusX: 4700,
|
||||
radiusY: 3900,
|
||||
rotationDeg: 8,
|
||||
scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-4`,
|
||||
depthLabel: "2.0~5.0m",
|
||||
color: "#8f8bff",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 3200,
|
||||
centerY: centerY + 3200,
|
||||
radiusX: 2600,
|
||||
radiusY: 2000,
|
||||
rotationDeg: 26,
|
||||
scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-5`,
|
||||
depthLabel: "5.0m 이상",
|
||||
color: "#f2528b",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 400,
|
||||
centerY: centerY - 3600,
|
||||
radiusX: 1200,
|
||||
radiusY: 1050,
|
||||
rotationDeg: -10,
|
||||
scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98],
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const normalizeDate = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!/^\d{8}$/.test(source)) return "";
|
||||
return `${source.slice(0, 4)}-${source.slice(4, 6)}-${source.slice(6, 8)}`;
|
||||
};
|
||||
|
||||
const normalizeHm = (value?: string | null) => {
|
||||
const digits = String(value ?? "").replace(/[^0-9]/g, "");
|
||||
if (!digits) return "";
|
||||
if (digits.length === 1) return `0${digits}:00`;
|
||||
if (digits.length === 2) return `${digits}:00`;
|
||||
if (digits.length === 3) return `0${digits.slice(0, 1)}:${digits.slice(1, 3)}`;
|
||||
return `${digits.slice(0, 2)}:${digits.slice(2, 4)}`;
|
||||
};
|
||||
|
||||
const normalizeDateTime = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) return "";
|
||||
const date = new Date(source);
|
||||
if (Number.isNaN(date.getTime())) return source.slice(0, 16).replace("T", " ");
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const dd = `${date.getDate()}`.padStart(2, "0");
|
||||
const hh = `${date.getHours()}`.padStart(2, "0");
|
||||
const mi = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
||||
};
|
||||
|
||||
const formatNumber = (value?: number | null, digits = 1, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) return `0${suffix}`;
|
||||
return `${numeric.toFixed(digits)}${suffix}`;
|
||||
};
|
||||
|
||||
const formatInteger = (value?: number | null, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) return `0${suffix}`;
|
||||
return `${numeric.toLocaleString("ko-KR")}${suffix}`;
|
||||
};
|
||||
|
||||
const buildCauseDetailName = (item: FldngHstryApiItem) => {
|
||||
const direct = String(item.dtlOcrnCsCn ?? "").trim();
|
||||
if (direct) return direct;
|
||||
const code = String(item.fldngCsDtlCd ?? "").trim();
|
||||
return code ? `원인코드 ${code}` : "-";
|
||||
};
|
||||
|
||||
const buildGradeLabel = (avgFloodDepthM: number) => {
|
||||
if (avgFloodDepthM >= 2) return "3등급";
|
||||
if (avgFloodDepthM >= 1) return "2등급";
|
||||
return "1등급";
|
||||
};
|
||||
|
||||
export const normalizeFldngHstryFilters = (
|
||||
filters: FldngHstryFilters,
|
||||
): FldngHstryFilters => ({
|
||||
ctpvCd: String(filters.ctpvCd ?? "0") || "0",
|
||||
sggCd: String(filters.sggCd ?? "0") || "0",
|
||||
floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4),
|
||||
disasterName: String(filters.disasterName ?? "").trim(),
|
||||
});
|
||||
|
||||
export const buildFldngHstryYearOptions = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return Array.from({ length: 30 }, (_, index) => String(currentYear - index));
|
||||
};
|
||||
|
||||
export const buildFldngHstryListQueryString = (
|
||||
filters: FldngHstryFilters,
|
||||
page: number,
|
||||
size: number,
|
||||
sort?: UiTableSort,
|
||||
) => {
|
||||
const normalized = normalizeFldngHstryFilters(filters);
|
||||
const params = new URLSearchParams({
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
});
|
||||
|
||||
if (normalized.ctpvCd !== "0") params.set("ctpvCd", normalized.ctpvCd.slice(0, 2));
|
||||
if (normalized.sggCd !== "0") params.set("sggCd", normalized.sggCd.slice(0, 5));
|
||||
if (normalized.floodYear) params.set("fldngYr", normalized.floodYear);
|
||||
if (normalized.disasterName) params.set("fldngDstrNm", normalized.disasterName);
|
||||
if (sort?.key && sort.direction) params.set("sort", `${sort.key},${sort.direction}`);
|
||||
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`;
|
||||
|
||||
export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`;
|
||||
|
||||
export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryItem => {
|
||||
const historyId = String(item.fid ?? "");
|
||||
const geometrySeed = buildGeometrySeed(historyId);
|
||||
const startDate = normalizeDate(item.bgngYmd);
|
||||
const endDate = normalizeDate(item.endYmd);
|
||||
const startTime = normalizeHm(item.fldngBgngHm);
|
||||
const endTime = normalizeHm(item.fldngEndHm);
|
||||
const avgFloodDepthM = Number(item.avgFldngWtlv ?? 0);
|
||||
const floodAreaHa = Number(item.fldngArea ?? 0);
|
||||
|
||||
return {
|
||||
...item,
|
||||
historyId,
|
||||
ctpvNm: "",
|
||||
sggNm: "",
|
||||
floodYear: String(item.fldngYr ?? "").trim(),
|
||||
exmnYear: String(item.exmnYr ?? "").trim(),
|
||||
disasterName: String(item.fldngDstrNm ?? "").trim(),
|
||||
districtName: String(item.fldngDstrctNm ?? "").trim(),
|
||||
floodAreaHa,
|
||||
avgFloodDepthM,
|
||||
floodDepthM: avgFloodDepthM,
|
||||
gradeLabel: buildGradeLabel(avgFloodDepthM),
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
floodHour: formatNumber(Number(item.fldngHr ?? 0), 1, "시간"),
|
||||
causeDetailCode: String(item.fldngCsDtlCd ?? "").trim(),
|
||||
causeDetailName: buildCauseDetailName(item),
|
||||
damageAmount: formatInteger(Number(item.damAmt ?? 0), "원"),
|
||||
rainStartDateTime: normalizeDateTime(item.rainBgngTm),
|
||||
rainEndDateTime: normalizeDateTime(item.rainEndTm),
|
||||
rainStartTime: normalizeDateTime(item.rainBgngTm).slice(11, 16),
|
||||
rainEndTime: normalizeDateTime(item.rainEndTm).slice(11, 16),
|
||||
totalRainfallMm: Number(item.rainTot ?? 0),
|
||||
rainfallIntensityMmHr: Number(item.rainStr ?? 0),
|
||||
areaRing: createIrregularRing({
|
||||
centerX: geometrySeed.centerX,
|
||||
centerY: geometrySeed.centerY,
|
||||
radiusX: geometrySeed.width / 2,
|
||||
radiusY: geometrySeed.height / 2,
|
||||
rotationDeg: 9,
|
||||
scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86],
|
||||
}),
|
||||
depthZones: createFldngHstryDepthZones(historyId, geometrySeed.centerX, geometrySeed.centerY),
|
||||
};
|
||||
};
|
||||
|
||||
export const toFldngHstryTableRows = (
|
||||
items: FldngHstryItem[],
|
||||
page: number,
|
||||
pageSize = FLDNG_HSTRY_PAGE_SIZE,
|
||||
): FldngHstryTableRow[] =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
rowNo: (page - 1) * pageSize + index + 1,
|
||||
floodYearDisplay: item.floodYear ? `${item.floodYear}년` : "-",
|
||||
floodHourDisplay: item.floodHour || "-",
|
||||
avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM),
|
||||
floodAreaDisplay: formatFloodArea(item.floodAreaHa),
|
||||
exmnYearDisplay: item.exmnYear ? `${item.exmnYear}년` : "-",
|
||||
damageAmountDisplay: item.damageAmount || "-",
|
||||
rainBgngTmDisplay: item.rainStartDateTime || "-",
|
||||
rainEndTmDisplay: item.rainEndDateTime || "-",
|
||||
rainTotDisplay: formatNumber(item.totalRainfallMm, 1),
|
||||
rainStrDisplay: formatNumber(item.rainfallIntensityMmHr, 1),
|
||||
}));
|
||||
|
||||
const resolveFldngHstryNodeName = (node: any) =>
|
||||
String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim();
|
||||
|
||||
const resolveFldngHstryLayerType = (node: any) =>
|
||||
String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim();
|
||||
|
||||
export const isFldngHstryNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return name === FLOOD_HISTORY_LAYER_NAME
|
||||
|| layerType === FLOOD_HISTORY_LAYER_TYPE
|
||||
|| layerType === FLOOD_HISTORY_PREVIEW_LAYER_TYPE;
|
||||
};
|
||||
|
||||
export const isFloodHistoryNode = isFldngHstryNode;
|
||||
export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = FLDNG_HSTRY_ROW_SELECT_MESSAGE;
|
||||
export const FLOOD_HISTORY_PAGE_SIZE = FLDNG_HSTRY_PAGE_SIZE;
|
||||
export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE = FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE;
|
||||
@@ -1,408 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
||||
import useStore from "@/store/useStore";
|
||||
import {
|
||||
buildFloodHistoryYearOptions,
|
||||
buildFloodHistoryTableRows,
|
||||
FLOOD_HISTORY_DEFAULT_FILTERS,
|
||||
FLOOD_HISTORY_DOWNLOAD_HEADERS,
|
||||
FLOOD_HISTORY_ITEMS,
|
||||
FLOOD_HISTORY_PAGE_SIZE,
|
||||
FLOOD_HISTORY_ROW_SELECT_MESSAGE,
|
||||
formatFloodArea,
|
||||
formatFloodDepth,
|
||||
normalizeFloodHistoryFilters,
|
||||
type FloodHistoryFilters,
|
||||
type FloodHistoryItem,
|
||||
} from "./floodHistoryTypes";
|
||||
|
||||
const resolveOptionText = (
|
||||
code: string,
|
||||
options: Array<{ text: string; val: string | number }>,
|
||||
prefixLength?: number,
|
||||
) => {
|
||||
const normalized = String(code ?? "").trim();
|
||||
if (!normalized || normalized === "0") return "";
|
||||
const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized;
|
||||
return options.find((item) => String(item.val).trim() === lookup)?.text ?? "";
|
||||
};
|
||||
|
||||
const GRID_COLUMNS = [
|
||||
{ id: "rowNo", title: "NO", width: "64px", align: "center" },
|
||||
{ id: "ctpvNm", title: "시도", width: "90px", align: "center" },
|
||||
{ id: "sggNm", title: "시군구", width: "100px", align: "center" },
|
||||
{ id: "startDateDisplay", title: "침수시작일자", width: "118px", align: "center" },
|
||||
{ id: "endDateDisplay", title: "침수종료일자", width: "118px", align: "center" },
|
||||
{ id: "startTimeDisplay", title: "침수시작시간", width: "98px", align: "center" },
|
||||
{ id: "endTimeDisplay", title: "침수종료시간", width: "98px", align: "center" },
|
||||
{ id: "floodYearDisplay", title: "침수년도", width: "96px", align: "center" },
|
||||
{ id: "floodHourDisplay", title: "침수시간", width: "100px", align: "center" },
|
||||
{ id: "disasterName", title: "침수재해명", width: "190px", align: "left" },
|
||||
{ id: "districtNameDisplay", title: "침수지구명", width: "150px", align: "left" },
|
||||
{ id: "causeCodeDisplay", title: "침수원인상세코드", width: "132px", align: "center" },
|
||||
{ id: "causeDetailDisplay", title: "침수원인상세", width: "190px", align: "left" },
|
||||
{ id: "avgFloodDepthDisplay", title: "평균침수위", width: "104px", align: "right" },
|
||||
{ id: "floodAreaDisplay", title: "침수면적", width: "104px", align: "right" },
|
||||
{ id: "exmnYearDisplay", title: "조사년도", width: "96px", align: "center" },
|
||||
{ id: "damageAmountDisplay", title: "피해금액", width: "132px", align: "right" },
|
||||
] as const;
|
||||
|
||||
const buildDownloadRows = (rows: FloodHistoryItem[]) =>
|
||||
rows.map((row, index) => ({
|
||||
rowNo: index + 1,
|
||||
ctpvNm: row.ctpvNm,
|
||||
sggNm: row.sggNm,
|
||||
disasterName: row.disasterName,
|
||||
exmnYearDisplay: `${row.exmnYear}년`,
|
||||
avgFloodDepthDisplay: formatFloodDepth(row.avgFloodDepthM),
|
||||
floodAreaDisplay: formatFloodArea(row.floodAreaHa),
|
||||
causeDetailDisplay: row.causeDetailName,
|
||||
startDateDisplay: row.startDate,
|
||||
endDateDisplay: row.endDate,
|
||||
startTimeDisplay: row.startTime,
|
||||
endTimeDisplay: row.endTime,
|
||||
causeCodeDisplay: row.causeDetailCode,
|
||||
districtNameDisplay: row.districtName,
|
||||
damageAmountDisplay: row.damageAmount,
|
||||
floodHourDisplay: row.floodHour,
|
||||
floodYearDisplay: `${row.floodYear}년`,
|
||||
}));
|
||||
|
||||
export default function FloodHistoryPopupPage() {
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
// 조회조건 draft는 popup 내부에서 사용자가 편집 중인 값이다.
|
||||
const [filters, setFilters] = useState<FloodHistoryFilters>(FLOOD_HISTORY_DEFAULT_FILTERS);
|
||||
// 적용된 조회조건과 선택행은 다운로드/페이지 이동에도 같은 기준을 유지하기 위해 분리한다.
|
||||
const [appliedFilters, setAppliedFilters] = useState<FloodHistoryFilters>(FLOOD_HISTORY_DEFAULT_FILTERS);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (ctpv.length === 0) void fetchCtpv();
|
||||
if (sgg.length === 0) void fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
|
||||
const floodYearOptions = useMemo(
|
||||
() => buildFloodHistoryYearOptions(FLOOD_HISTORY_ITEMS),
|
||||
[],
|
||||
);
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
|
||||
[filters.ctpvCd, sgg],
|
||||
);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
const normalized = normalizeFloodHistoryFilters(appliedFilters);
|
||||
const selectedCtpvName = resolveOptionText(normalized.ctpvCd, ctpv as any, 2);
|
||||
const selectedSggName = resolveOptionText(normalized.sggCd, sgg as any, 5);
|
||||
const disasterToken = normalized.disasterName.toLocaleLowerCase("ko-KR");
|
||||
|
||||
return FLOOD_HISTORY_ITEMS.filter((item) => {
|
||||
if (selectedCtpvName && item.ctpvNm !== selectedCtpvName) return false;
|
||||
if (selectedSggName && item.sggNm !== selectedSggName) return false;
|
||||
if (normalized.floodYear && item.floodYear !== normalized.floodYear) return false;
|
||||
if (
|
||||
disasterToken &&
|
||||
!item.disasterName.toLocaleLowerCase("ko-KR").includes(disasterToken) &&
|
||||
!item.districtName.toLocaleLowerCase("ko-KR").includes(disasterToken)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).sort((left, right) => {
|
||||
if (left.floodYear !== right.floodYear) {
|
||||
return Number(right.floodYear) - Number(left.floodYear);
|
||||
}
|
||||
return left.disasterName.localeCompare(right.disasterName, "ko");
|
||||
});
|
||||
}, [appliedFilters, ctpv, sgg]);
|
||||
|
||||
const pagedRows = useMemo(() => {
|
||||
const start = (currentPage - 1) * FLOOD_HISTORY_PAGE_SIZE;
|
||||
const end = start + FLOOD_HISTORY_PAGE_SIZE;
|
||||
return buildFloodHistoryTableRows(
|
||||
filteredRows.slice(start, end),
|
||||
currentPage,
|
||||
FLOOD_HISTORY_PAGE_SIZE,
|
||||
);
|
||||
}, [currentPage, filteredRows]);
|
||||
|
||||
const handleSearch = () => {
|
||||
const normalized = normalizeFloodHistoryFilters(filters);
|
||||
|
||||
if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") {
|
||||
alert("시도를 먼저 선택하세요.");
|
||||
return;
|
||||
}
|
||||
if (normalized.floodYear && normalized.floodYear.length !== 4) {
|
||||
alert("침수발생년도는 4자리 연도로 입력하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setAppliedFilters(normalized);
|
||||
setCurrentPage(1);
|
||||
setSelectedHistoryId("");
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (filteredRows.length === 0) {
|
||||
alert("다운로드할 침수발생이력 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: buildDownloadRows(filteredRows) as unknown as JSON[],
|
||||
fileName: `침수흔적도_${new Date().toISOString().slice(0, 10)}.xlsx`,
|
||||
sheetNameArray: ["침수흔적도"],
|
||||
headerRange: "A1:Q1",
|
||||
headerMapping: FLOOD_HISTORY_DOWNLOAD_HEADERS,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRowDoubleClick = (row: FloodHistoryItem) => {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: FLOOD_HISTORY_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
openDetail: false,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
window.close();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: FloodHistoryItem) => {
|
||||
setSelectedHistoryId(row.historyId);
|
||||
// popup을 유지한 채 메인 지도에 mock 침수흔적/침수심 preview를 먼저 보여준다.
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: FLOOD_HISTORY_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
openDetail: false,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="sn-popup-page">
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">침수흔적도 목록</h1>
|
||||
<div />
|
||||
</header>
|
||||
|
||||
<section className="sn-popup-section">
|
||||
<div className="sn-popup-filter-panel-compact">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-[180px_180px_180px_minmax(0,1fr)_auto] md:items-end">
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시도</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시군구</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
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="sn-popup-field">
|
||||
<div className="sn-popup-field-label">침수발생년도</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
value={filters.floodYear}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
floodYear: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{floodYearOptions.map((year) => (
|
||||
<option key={year} value={year}>
|
||||
{year}년
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">침수재해명</div>
|
||||
<Input
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={filters.disasterName}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
disasterName: event.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") handleSearch();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="sn-popup-actions md:pr-3">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="sn-popup-primary-button !text-[1.1rem]"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="sn-popup-primary-button !text-[1.1rem]"
|
||||
onClick={() => void handleDownload()}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
침수흔적도 조회 결과 {filteredRows.length.toLocaleString("ko-KR")}건
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="card !m-[1.2rem] !mt-0 min-h-0 flex-1 overflow-x-auto overflow-y-auto">
|
||||
<Table variant="secondary" className="w-max min-w-full">
|
||||
<colgroup>
|
||||
{GRID_COLUMNS.map((head) => (
|
||||
<col key={head.id} style={{ width: head.width }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<TableHeader className="font-bold">
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">NO</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">시도</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">시군구</TableHead>
|
||||
<TableHead colSpan={4} className="whitespace-nowrap">강우 분석정보</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수재해명</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수년도</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수시간</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수지구명</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수원인상세코드</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수원인상세</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">평균침수위</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">침수면적</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">조사년도</TableHead>
|
||||
<TableHead rowSpan={2} className="whitespace-nowrap">피해금액</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="whitespace-nowrap">침수시작일자</TableHead>
|
||||
<TableHead className="whitespace-nowrap">침수종료일자</TableHead>
|
||||
<TableHead className="whitespace-nowrap">침수시작시간</TableHead>
|
||||
<TableHead className="whitespace-nowrap">침수종료시간</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="text-center">
|
||||
{pagedRows.length > 0 ? (
|
||||
pagedRows.map((row) => {
|
||||
const isSelected = selectedHistoryId === row.historyId;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.historyId}
|
||||
className={`cursor-pointer ${isSelected ? "sn-popup-grid-row-selected" : "sn-popup-grid-row-hover"}`}
|
||||
onClick={() => handleRowClick(row)}
|
||||
onDoubleClick={() => handleRowDoubleClick(row)}
|
||||
>
|
||||
<TableCell>{row.rowNo}</TableCell>
|
||||
<TableCell>{row.ctpvNm}</TableCell>
|
||||
<TableCell>{row.sggNm}</TableCell>
|
||||
<TableCell>{row.startDateDisplay}</TableCell>
|
||||
<TableCell>{row.endDateDisplay}</TableCell>
|
||||
<TableCell>{row.startTimeDisplay}</TableCell>
|
||||
<TableCell>{row.endTimeDisplay}</TableCell>
|
||||
<TableCell className="text-left">{row.disasterName}</TableCell>
|
||||
<TableCell>{row.floodYearDisplay}</TableCell>
|
||||
<TableCell>{row.floodHourDisplay}</TableCell>
|
||||
<TableCell className="text-left">{row.districtNameDisplay}</TableCell>
|
||||
<TableCell>{row.causeCodeDisplay}</TableCell>
|
||||
<TableCell className="text-left">{row.causeDetailDisplay}</TableCell>
|
||||
<TableCell className="text-right">{row.avgFloodDepthDisplay}</TableCell>
|
||||
<TableCell className="text-right">{row.floodAreaDisplay}</TableCell>
|
||||
<TableCell>{row.exmnYearDisplay}</TableCell>
|
||||
<TableCell className="text-right">{row.damageAmountDisplay}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={GRID_COLUMNS.length} className="sn-popup-empty-cell h-24 text-center">
|
||||
조회된 데이터가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{filteredRows.length > 0 ? (
|
||||
<UiPageNations
|
||||
total={filteredRows.length}
|
||||
cur={currentPage}
|
||||
pageSize={FLOOD_HISTORY_PAGE_SIZE}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="sn-popup-help-text">
|
||||
행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export const FLOOD_HISTORY_POPUP_KEY = "floodHistoryPopup";
|
||||
export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT";
|
||||
export const FLOOD_HISTORY_LAYER_TYPE = "tb_sn_flood_trace_mock";
|
||||
export const FLOOD_HISTORY_LAYER_NAME = "침수흔적도";
|
||||
export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
|
||||
export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
|
||||
export const FLOOD_HISTORY_PAGE_SIZE = 10;
|
||||
|
||||
export type FloodHistoryFilters = {
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
floodYear: string;
|
||||
disasterName: string;
|
||||
};
|
||||
|
||||
export type FloodHistoryDepthZone = {
|
||||
zoneId: string;
|
||||
depthLabel: string;
|
||||
color: string;
|
||||
ring: [number, number][];
|
||||
};
|
||||
|
||||
export type FloodHistoryItem = {
|
||||
historyId: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
floodYear: string;
|
||||
exmnYear: string;
|
||||
disasterName: string;
|
||||
districtName: string;
|
||||
floodAreaHa: number;
|
||||
avgFloodDepthM: number;
|
||||
floodDepthM: number;
|
||||
gradeLabel: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
floodHour: string;
|
||||
causeDetailCode: string;
|
||||
causeDetailName: string;
|
||||
damageAmount: string;
|
||||
rainStartTime: string;
|
||||
rainEndTime: string;
|
||||
totalRainfallMm: number;
|
||||
rainfallIntensityMmHr: number;
|
||||
areaRing: [number, number][];
|
||||
depthZones: FloodHistoryDepthZone[];
|
||||
};
|
||||
|
||||
export type FloodHistoryTableRow = FloodHistoryItem & {
|
||||
rowNo: number;
|
||||
exmnYearDisplay: string;
|
||||
avgFloodDepthDisplay: string;
|
||||
floodAreaDisplay: string;
|
||||
causeDetailDisplay: string;
|
||||
startDateDisplay: string;
|
||||
endDateDisplay: string;
|
||||
startTimeDisplay: string;
|
||||
endTimeDisplay: string;
|
||||
causeCodeDisplay: string;
|
||||
districtNameDisplay: string;
|
||||
damageAmountDisplay: string;
|
||||
floodHourDisplay: string;
|
||||
floodYearDisplay: string;
|
||||
};
|
||||
|
||||
export const FLOOD_HISTORY_DEFAULT_FILTERS: FloodHistoryFilters = {
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
floodYear: "",
|
||||
disasterName: "",
|
||||
};
|
||||
|
||||
export const FLOOD_HISTORY_DOWNLOAD_HEADERS: Record<string, string> = {
|
||||
rowNo: "NO",
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
disasterName: "침수재해명",
|
||||
exmnYearDisplay: "조사년도",
|
||||
avgFloodDepthDisplay: "평균침수위",
|
||||
floodAreaDisplay: "침수면적",
|
||||
causeDetailDisplay: "침수원인상세",
|
||||
startDateDisplay: "침수시작일자",
|
||||
endDateDisplay: "침수종료일자",
|
||||
startTimeDisplay: "침수시작시간",
|
||||
endTimeDisplay: "침수종료시간",
|
||||
causeCodeDisplay: "침수원인상세코드",
|
||||
districtNameDisplay: "침수지구명",
|
||||
damageAmountDisplay: "피해금액",
|
||||
floodHourDisplay: "침수시간",
|
||||
floodYearDisplay: "침수년도",
|
||||
};
|
||||
|
||||
export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [
|
||||
{ color: "#fff27a", label: "0.0~0.5m" },
|
||||
{ color: "#c2f13d", label: "0.5~1.0m" },
|
||||
{ color: "#67d8ff", label: "1.0~2.0m" },
|
||||
{ color: "#8f8bff", label: "2.0~5.0m" },
|
||||
{ color: "#f2528b", label: "5.0m 이상" },
|
||||
] as const;
|
||||
|
||||
const toRadian = (degree: number) => (degree * Math.PI) / 180;
|
||||
|
||||
const createIrregularRing = ({
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
scales,
|
||||
rotationDeg = 0,
|
||||
}: {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
scales: number[];
|
||||
rotationDeg?: number;
|
||||
}): [number, number][] => {
|
||||
const rotation = toRadian(rotationDeg);
|
||||
const points = scales.map((scale, index) => {
|
||||
const angle = rotation + (Math.PI * 2 * index) / scales.length;
|
||||
const jitterX = radiusX * scale * Math.cos(angle);
|
||||
const jitterY = radiusY * scale * Math.sin(angle);
|
||||
return [centerX + jitterX, centerY + jitterY] as [number, number];
|
||||
});
|
||||
|
||||
return [...points, points[0]];
|
||||
};
|
||||
|
||||
const createDepthZones = (
|
||||
historyId: string,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): FloodHistoryDepthZone[] => {
|
||||
return [
|
||||
{
|
||||
zoneId: `${historyId}-depth-1`,
|
||||
depthLabel: "0.0~0.5m",
|
||||
color: "#fff27a",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX - 5200,
|
||||
centerY: centerY - 2600,
|
||||
radiusX: 3600,
|
||||
radiusY: 2900,
|
||||
rotationDeg: -18,
|
||||
scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-2`,
|
||||
depthLabel: "0.5~1.0m",
|
||||
color: "#c2f13d",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 2600,
|
||||
centerY: centerY - 1200,
|
||||
radiusX: 4300,
|
||||
radiusY: 3600,
|
||||
rotationDeg: 12,
|
||||
scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-3`,
|
||||
depthLabel: "1.0~2.0m",
|
||||
color: "#67d8ff",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX - 600,
|
||||
centerY: centerY + 2600,
|
||||
radiusX: 5600,
|
||||
radiusY: 4700,
|
||||
rotationDeg: 8,
|
||||
scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-4`,
|
||||
depthLabel: "2.0~5.0m",
|
||||
color: "#8f8bff",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 4200,
|
||||
centerY: centerY + 5200,
|
||||
radiusX: 3200,
|
||||
radiusY: 2500,
|
||||
rotationDeg: 26,
|
||||
scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-5`,
|
||||
depthLabel: "5.0m 이상",
|
||||
color: "#f2528b",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 600,
|
||||
centerY: centerY - 5200,
|
||||
radiusX: 1500,
|
||||
radiusY: 1350,
|
||||
rotationDeg: -10,
|
||||
scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98],
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createHistory = (
|
||||
item: Omit<FloodHistoryItem, "areaRing" | "depthZones"> & {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
): FloodHistoryItem => {
|
||||
const { centerX, centerY, width, height, ...detail } = item;
|
||||
|
||||
return {
|
||||
...detail,
|
||||
areaRing: createIrregularRing({
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX: width / 2,
|
||||
radiusY: height / 2,
|
||||
rotationDeg: 9,
|
||||
scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86],
|
||||
}),
|
||||
depthZones: createDepthZones(detail.historyId, centerX, centerY),
|
||||
};
|
||||
};
|
||||
|
||||
export const FLOOD_HISTORY_ITEMS: FloodHistoryItem[] = [
|
||||
createHistory({
|
||||
historyId: "FH-2024-001",
|
||||
ctpvNm: "부산광역시",
|
||||
sggNm: "중구",
|
||||
floodYear: "2024",
|
||||
exmnYear: "2025",
|
||||
disasterName: "중앙동 저지대 침수",
|
||||
districtName: "중앙동 저지대",
|
||||
floodAreaHa: 1.8,
|
||||
avgFloodDepthM: 0.62,
|
||||
floodDepthM: 0.62,
|
||||
gradeLabel: "1등급",
|
||||
startDate: "2024-07-18",
|
||||
endDate: "2024-07-18",
|
||||
startTime: "14:10",
|
||||
endTime: "18:20",
|
||||
floodHour: "4.2시간",
|
||||
causeDetailCode: "FC-01",
|
||||
causeDetailName: "집중강우",
|
||||
damageAmount: "120,000,000원",
|
||||
rainStartTime: "13:50",
|
||||
rainEndTime: "18:20",
|
||||
totalRainfallMm: 91,
|
||||
rainfallIntensityMmHr: 31,
|
||||
centerX: 1122000,
|
||||
centerY: 1705000,
|
||||
width: 18000,
|
||||
height: 14000,
|
||||
}),
|
||||
createHistory({
|
||||
historyId: "FH-2023-002",
|
||||
ctpvNm: "경상남도",
|
||||
sggNm: "창원시",
|
||||
floodYear: "2023",
|
||||
exmnYear: "2024",
|
||||
disasterName: "팔용동 공단 배수지연 침수",
|
||||
districtName: "팔용동 공단",
|
||||
floodAreaHa: 4.8,
|
||||
avgFloodDepthM: 1.92,
|
||||
floodDepthM: 1.92,
|
||||
gradeLabel: "2등급",
|
||||
startDate: "2023-08-11",
|
||||
endDate: "2023-08-11",
|
||||
startTime: "09:40",
|
||||
endTime: "15:10",
|
||||
floodHour: "5.5시간",
|
||||
causeDetailCode: "FC-04",
|
||||
causeDetailName: "배수시설 처리능력 초과",
|
||||
damageAmount: "340,000,000원",
|
||||
rainStartTime: "08:55",
|
||||
rainEndTime: "15:05",
|
||||
totalRainfallMm: 138,
|
||||
rainfallIntensityMmHr: 42,
|
||||
centerX: 1081000,
|
||||
centerY: 1778000,
|
||||
width: 25000,
|
||||
height: 17000,
|
||||
}),
|
||||
createHistory({
|
||||
historyId: "FH-2022-003",
|
||||
ctpvNm: "울산광역시",
|
||||
sggNm: "남구",
|
||||
floodYear: "2022",
|
||||
exmnYear: "2023",
|
||||
disasterName: "태화강 제내지 도로 침수",
|
||||
districtName: "여천동 하류부",
|
||||
floodAreaHa: 3.1,
|
||||
avgFloodDepthM: 1.15,
|
||||
floodDepthM: 1.15,
|
||||
gradeLabel: "2등급",
|
||||
startDate: "2022-09-06",
|
||||
endDate: "2022-09-06",
|
||||
startTime: "07:20",
|
||||
endTime: "12:40",
|
||||
floodHour: "5.3시간",
|
||||
causeDetailCode: "FC-02",
|
||||
causeDetailName: "하천 수위 상승",
|
||||
damageAmount: "210,000,000원",
|
||||
rainStartTime: "06:30",
|
||||
rainEndTime: "11:50",
|
||||
totalRainfallMm: 117,
|
||||
rainfallIntensityMmHr: 37,
|
||||
centerX: 1168000,
|
||||
centerY: 1811000,
|
||||
width: 22000,
|
||||
height: 16000,
|
||||
}),
|
||||
createHistory({
|
||||
historyId: "FH-2021-004",
|
||||
ctpvNm: "부산광역시",
|
||||
sggNm: "서구",
|
||||
floodYear: "2021",
|
||||
exmnYear: "2022",
|
||||
disasterName: "서대신동 급경사지 유출 침수",
|
||||
districtName: "서대신동 일원",
|
||||
floodAreaHa: 2.4,
|
||||
avgFloodDepthM: 0.85,
|
||||
floodDepthM: 0.85,
|
||||
gradeLabel: "1등급",
|
||||
startDate: "2021-07-07",
|
||||
endDate: "2021-07-07",
|
||||
startTime: "16:00",
|
||||
endTime: "20:10",
|
||||
floodHour: "4.1시간",
|
||||
causeDetailCode: "FC-03",
|
||||
causeDetailName: "우수관로 월류",
|
||||
damageAmount: "175,000,000원",
|
||||
rainStartTime: "15:20",
|
||||
rainEndTime: "19:50",
|
||||
totalRainfallMm: 104,
|
||||
rainfallIntensityMmHr: 29,
|
||||
centerX: 1111000,
|
||||
centerY: 1709000,
|
||||
width: 20000,
|
||||
height: 14500,
|
||||
}),
|
||||
createHistory({
|
||||
historyId: "FH-2020-005",
|
||||
ctpvNm: "부산광역시",
|
||||
sggNm: "영도구",
|
||||
floodYear: "2020",
|
||||
exmnYear: "2021",
|
||||
disasterName: "청학동 해안도로 월파 침수",
|
||||
districtName: "청학동 해안부",
|
||||
floodAreaHa: 1.2,
|
||||
avgFloodDepthM: 0.48,
|
||||
floodDepthM: 0.48,
|
||||
gradeLabel: "1등급",
|
||||
startDate: "2020-10-22",
|
||||
endDate: "2020-10-22",
|
||||
startTime: "03:10",
|
||||
endTime: "06:30",
|
||||
floodHour: "3.3시간",
|
||||
causeDetailCode: "FC-06",
|
||||
causeDetailName: "해안 월파",
|
||||
damageAmount: "82,000,000원",
|
||||
rainStartTime: "02:40",
|
||||
rainEndTime: "05:50",
|
||||
totalRainfallMm: 55,
|
||||
rainfallIntensityMmHr: 18,
|
||||
centerX: 1129000,
|
||||
centerY: 1695000,
|
||||
width: 15000,
|
||||
height: 12000,
|
||||
}),
|
||||
createHistory({
|
||||
historyId: "FH-2019-006",
|
||||
ctpvNm: "경상남도",
|
||||
sggNm: "김해시",
|
||||
floodYear: "2019",
|
||||
exmnYear: "2020",
|
||||
disasterName: "장유 배수분구 내수 침수",
|
||||
districtName: "장유 배수분구",
|
||||
floodAreaHa: 2.9,
|
||||
avgFloodDepthM: 0.96,
|
||||
floodDepthM: 0.96,
|
||||
gradeLabel: "1등급",
|
||||
startDate: "2019-09-21",
|
||||
endDate: "2019-09-21",
|
||||
startTime: "11:30",
|
||||
endTime: "16:15",
|
||||
floodHour: "4.8시간",
|
||||
causeDetailCode: "FC-05",
|
||||
causeDetailName: "배수펌프 지연 가동",
|
||||
damageAmount: "143,000,000원",
|
||||
rainStartTime: "10:45",
|
||||
rainEndTime: "15:50",
|
||||
totalRainfallMm: 88,
|
||||
rainfallIntensityMmHr: 24,
|
||||
centerX: 1098000,
|
||||
centerY: 1734000,
|
||||
width: 21000,
|
||||
height: 15000,
|
||||
}),
|
||||
];
|
||||
|
||||
export const normalizeFloodHistoryFilters = (
|
||||
filters: FloodHistoryFilters,
|
||||
): FloodHistoryFilters => ({
|
||||
ctpvCd: String(filters.ctpvCd ?? "0") || "0",
|
||||
sggCd: String(filters.sggCd ?? "0") || "0",
|
||||
floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4),
|
||||
disasterName: String(filters.disasterName ?? "").trim(),
|
||||
});
|
||||
|
||||
export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`;
|
||||
|
||||
export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`;
|
||||
|
||||
export const buildFloodHistoryTableRows = (
|
||||
items: FloodHistoryItem[],
|
||||
page: number,
|
||||
pageSize = FLOOD_HISTORY_PAGE_SIZE,
|
||||
): FloodHistoryTableRow[] =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
rowNo: (page - 1) * pageSize + index + 1,
|
||||
exmnYearDisplay: `${item.exmnYear}년`,
|
||||
avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM),
|
||||
floodAreaDisplay: formatFloodArea(item.floodAreaHa),
|
||||
causeDetailDisplay: item.causeDetailName,
|
||||
startDateDisplay: item.startDate,
|
||||
endDateDisplay: item.endDate,
|
||||
startTimeDisplay: item.startTime,
|
||||
endTimeDisplay: item.endTime,
|
||||
causeCodeDisplay: item.causeDetailCode,
|
||||
districtNameDisplay: item.districtName,
|
||||
damageAmountDisplay: item.damageAmount,
|
||||
floodHourDisplay: item.floodHour,
|
||||
floodYearDisplay: `${item.floodYear}년`,
|
||||
}));
|
||||
|
||||
export const buildFloodHistoryYearOptions = (items: FloodHistoryItem[]) =>
|
||||
Array.from(new Set(items.map((item) => item.floodYear)))
|
||||
.sort((left, right) => Number(right) - Number(left));
|
||||
|
||||
const resolveFloodHistoryNodeName = (node: any) =>
|
||||
String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim();
|
||||
|
||||
const resolveFloodHistoryLayerType = (node: any) =>
|
||||
String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim();
|
||||
|
||||
export const isFloodHistoryNode = (node: any) => {
|
||||
const name = resolveFloodHistoryNodeName(node);
|
||||
const layerType = resolveFloodHistoryLayerType(node);
|
||||
return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE;
|
||||
};
|
||||
@@ -32,7 +32,7 @@ import Fill from "ol/style/Fill.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import { getLayerContentComponent } from "../common";
|
||||
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import { FldngWtlvMapCol, FldngWtlvMapCol2, SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
@@ -61,6 +61,12 @@ const linkedFacilityHighlightStyle = new Style({
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
const normalizeFldngWtlvMapDisplayId = (value: unknown) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
const prefix = "tb_ms_fldng_wtlv_map.";
|
||||
return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
|
||||
};
|
||||
|
||||
type MngInstOption = {
|
||||
mngInstCd: string;
|
||||
lwrInstNm: string;
|
||||
@@ -318,6 +324,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_fldng_wtlv_map': {
|
||||
result = '침수흔적도';
|
||||
setHeadlist(FldngWtlvMapCol);
|
||||
setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.values_.fid ?? featureId));
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
result = '우수토실';
|
||||
setHeadlist(stofcbrCol);
|
||||
@@ -381,6 +393,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_fldng_wtlv_map': {
|
||||
result = '침수흔적도';
|
||||
setHeadlist(FldngWtlvMapCol2);
|
||||
setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.fid));
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
result = '우수토실';
|
||||
setHeadlist(stofcbrCol2)
|
||||
@@ -478,6 +496,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
case '하수처리구역':
|
||||
setButtonChk(false)
|
||||
break;
|
||||
case '침수흔적도':
|
||||
setButtonChk(false)
|
||||
break;
|
||||
case '우수토실':
|
||||
setButtonChk(true)
|
||||
setIsMainSsc(false)
|
||||
|
||||
@@ -83,7 +83,7 @@ import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap
|
||||
import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap";
|
||||
import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel";
|
||||
import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel";
|
||||
import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel";
|
||||
import FldngHstryDetailPanel from "./components/fldngHstry/FldngHstryDetailPanel";
|
||||
import {
|
||||
buildSinkholeDefaultYearRange,
|
||||
buildSinkholeFiltersFromYearRange,
|
||||
@@ -113,12 +113,13 @@ import {
|
||||
import {
|
||||
FLOOD_HISTORY_AREA_LAYER_ID,
|
||||
FLOOD_HISTORY_DEPTH_LAYER_ID,
|
||||
FLOOD_HISTORY_PREVIEW_LAYER_TYPE,
|
||||
FLOOD_HISTORY_LAYER_TYPE,
|
||||
FLOOD_HISTORY_ROW_SELECT_MESSAGE,
|
||||
FLOOD_HISTORY_POPUP_KEY,
|
||||
isFloodHistoryNode,
|
||||
type FloodHistoryItem,
|
||||
} from "./components/floodHistory/floodHistoryTypes";
|
||||
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
isFldngHstryNode,
|
||||
type FldngHstryItem,
|
||||
} from "./components/fldngHstry/fldngHstryTypes";
|
||||
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
import { WATER_DEPTH_LAYER_TYPES, extractWaterDepthLegendItems } from "./waterDepth/waterDepth";
|
||||
@@ -141,6 +142,7 @@ const AnalysisPopupMenu = dynamic(
|
||||
|
||||
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS";
|
||||
const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
@@ -911,12 +913,14 @@ const getRightPanelFeatureTitle = (data: any) => {
|
||||
return "펌프장";
|
||||
case "tb_ms_stofcbr":
|
||||
return "우수토실";
|
||||
case "tb_il_swage_prcs_fclt":
|
||||
return "처리시설";
|
||||
case "tb_ms_swage_prcs_zone":
|
||||
return "처리구역";
|
||||
case "om_dt_untrt_swg_rain":
|
||||
return "중점우수";
|
||||
case "tb_il_swage_prcs_fclt":
|
||||
return "처리시설";
|
||||
case "tb_ms_swage_prcs_zone":
|
||||
return "처리구역";
|
||||
case "tb_ms_fldng_wtlv_map":
|
||||
return "침수흔적도";
|
||||
case "om_dt_untrt_swg_rain":
|
||||
return "중점우수";
|
||||
default:
|
||||
return String(data?.lyrNm ?? data?.values_?.lyrNm ?? "상세").trim() || "상세";
|
||||
}
|
||||
@@ -973,7 +977,7 @@ const getRightPanelFeatureDisplayLabel = (data: any, category: RightPanelTabCate
|
||||
return getRightPanelFeatureTitle(data);
|
||||
};
|
||||
|
||||
const createFloodHistoryAreaStyle = (label: string) =>
|
||||
const createFldngHstryAreaStyle = (label: string) =>
|
||||
new Style({
|
||||
fill: new Fill({
|
||||
color: "rgba(30, 136, 229, 0.16)",
|
||||
@@ -993,7 +997,7 @@ const createFloodHistoryAreaStyle = (label: string) =>
|
||||
zIndex: 9994,
|
||||
});
|
||||
|
||||
const createFloodHistoryDepthStyle = (label: string, color: string) =>
|
||||
const createFldngHstryDepthStyle = (label: string, color: string) =>
|
||||
new Style({
|
||||
fill: new Fill({
|
||||
color,
|
||||
@@ -2544,7 +2548,7 @@ const Map = () => {
|
||||
const [checkedLayerLegendVersion, setCheckedLayerLegendVersion] = useState(0);
|
||||
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
|
||||
const [isSinkholeChecked, setIsSinkholeChecked] = useState(false);
|
||||
const [isFloodHistoryChecked, setIsFloodHistoryChecked] = useState(false);
|
||||
const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false);
|
||||
const [swagePrcsFcltLayerMode, setSwagePrcsFcltLayerMode] = useState<SwagePrcsFcltLayerMode>("all");
|
||||
const [activeLayerSettingOrder, setActiveLayerSettingOrder] = useState<string[]>([]);
|
||||
const [draftAgedPipeFilters, setDraftAgedPipeFilters] =
|
||||
@@ -2559,8 +2563,8 @@ const Map = () => {
|
||||
);
|
||||
const [isSinkholePopupFilterActive, setIsSinkholePopupFilterActive] = useState(false);
|
||||
const [sinkholeRadiusMeters, setSinkholeRadiusMeters] = useState(SINKHOLE_DEFAULT_RADIUS);
|
||||
const [selectedFloodHistoryDetail, setSelectedFloodHistoryDetail] = useState<FloodHistoryItem | null>(null);
|
||||
const [floodHistoryDetailPopupPosition, setFloodHistoryDetailPopupPosition] = useState<{ left: number; top: number } | null>(null);
|
||||
const [selectedFldngHstryDetail, setSelectedFldngHstryDetail] = useState<FldngHstryItem | null>(null);
|
||||
const [fldngHstryDetailPopupPosition, setFldngHstryDetailPopupPosition] = useState<{ left: number; top: number } | null>(null);
|
||||
const [selectedSinkholeDetail, setSelectedSinkholeDetail] = useState<SinkholeDetail | null>(null);
|
||||
const [selectedSinkholeNearbyPipes, setSelectedSinkholeNearbyPipes] = useState<SinkholeNearbyPipe[]>([]);
|
||||
const [lastSinkholeSyncSummary, setLastSinkholeSyncSummary] = useState<SinkholeSyncSummary | null>(null);
|
||||
@@ -2593,8 +2597,8 @@ const Map = () => {
|
||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||
const sinkholePopupWindowRef = useRef<Window | null>(null);
|
||||
// 침수발생이력 overlay layer는 지도 드래그 중에도 유지돼야 하므로 React state 대신 ref로 보관한다.
|
||||
const floodHistoryAreaLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const floodHistoryDepthLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const fldngHstryAreaLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const fldngHstryDepthLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
const sinkholePopupCloseWatcherRef = useRef<number | null>(null);
|
||||
const pipeAnalysisPopupWindowRef = useRef<Window | null>(null);
|
||||
const gisEditSessionPopupWindowRef = useRef<Window | null>(null);
|
||||
@@ -4289,10 +4293,10 @@ const Map = () => {
|
||||
[clearSinkholeFocusedPipeLayer, ensureSinkholeFocusedPipeLayer],
|
||||
);
|
||||
|
||||
const ensureFloodHistoryAreaSource = useCallback(() => {
|
||||
const ensureFldngHstryAreaSource = useCallback(() => {
|
||||
if (!mapReady || !gisMaps.current) return null;
|
||||
|
||||
if (!floodHistoryAreaLayerRef.current) {
|
||||
if (!fldngHstryAreaLayerRef.current) {
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
@@ -4300,19 +4304,19 @@ const Map = () => {
|
||||
});
|
||||
layer.set("id", FLOOD_HISTORY_AREA_LAYER_ID);
|
||||
layer.set("lyrNm", "침수흔적도");
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
floodHistoryAreaLayerRef.current = layer;
|
||||
fldngHstryAreaLayerRef.current = layer;
|
||||
}
|
||||
|
||||
return floodHistoryAreaLayerRef.current.getSource();
|
||||
return fldngHstryAreaLayerRef.current.getSource();
|
||||
}, [mapReady]);
|
||||
|
||||
const ensureFloodHistoryDepthSource = useCallback(() => {
|
||||
const ensureFldngHstryDepthSource = useCallback(() => {
|
||||
if (!mapReady || !gisMaps.current) return null;
|
||||
|
||||
if (!floodHistoryDepthLayerRef.current) {
|
||||
if (!fldngHstryDepthLayerRef.current) {
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
@@ -4320,24 +4324,24 @@ const Map = () => {
|
||||
});
|
||||
layer.set("id", FLOOD_HISTORY_DEPTH_LAYER_ID);
|
||||
layer.set("lyrNm", "침수흔적도");
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
floodHistoryDepthLayerRef.current = layer;
|
||||
fldngHstryDepthLayerRef.current = layer;
|
||||
}
|
||||
|
||||
return floodHistoryDepthLayerRef.current.getSource();
|
||||
return fldngHstryDepthLayerRef.current.getSource();
|
||||
}, [mapReady]);
|
||||
|
||||
const clearFloodHistoryLayers = useCallback(() => {
|
||||
floodHistoryAreaLayerRef.current?.getSource?.()?.clear?.();
|
||||
floodHistoryDepthLayerRef.current?.getSource?.()?.clear?.();
|
||||
const clearFldngHstryLayers = useCallback(() => {
|
||||
fldngHstryAreaLayerRef.current?.getSource?.()?.clear?.();
|
||||
fldngHstryDepthLayerRef.current?.getSource?.()?.clear?.();
|
||||
}, []);
|
||||
|
||||
const renderSelectedFloodHistory = useCallback(
|
||||
(detail: FloodHistoryItem | null) => {
|
||||
const areaSource = ensureFloodHistoryAreaSource();
|
||||
const depthSource = ensureFloodHistoryDepthSource();
|
||||
const renderSelectedFldngHstry = useCallback(
|
||||
(detail: FldngHstryItem | null) => {
|
||||
const areaSource = ensureFldngHstryAreaSource();
|
||||
const depthSource = ensureFldngHstryDepthSource();
|
||||
if (!areaSource || !depthSource) return;
|
||||
|
||||
areaSource.clear();
|
||||
@@ -4348,21 +4352,21 @@ const Map = () => {
|
||||
const areaFeature = new Feature({
|
||||
geometry: new Polygon([detail.areaRing]),
|
||||
});
|
||||
areaFeature.setStyle(createFloodHistoryAreaStyle(`${detail.floodYear}년 ${detail.districtName}`));
|
||||
areaFeature.setStyle(createFldngHstryAreaStyle(`${detail.floodYear}년 ${detail.districtName}`));
|
||||
areaSource.addFeature(areaFeature);
|
||||
|
||||
detail.depthZones.forEach((zone) => {
|
||||
const zoneFeature = new Feature({
|
||||
geometry: new Polygon([zone.ring]),
|
||||
});
|
||||
zoneFeature.setStyle(createFloodHistoryDepthStyle(zone.depthLabel, zone.color));
|
||||
zoneFeature.setStyle(createFldngHstryDepthStyle(zone.depthLabel, zone.color));
|
||||
depthSource.addFeature(zoneFeature);
|
||||
});
|
||||
},
|
||||
[ensureFloodHistoryAreaSource, ensureFloodHistoryDepthSource],
|
||||
[ensureFldngHstryAreaSource, ensureFldngHstryDepthSource],
|
||||
);
|
||||
|
||||
const focusFloodHistoryOnMap = useCallback((detail: FloodHistoryItem) => {
|
||||
const focusFldngHstryOnMap = useCallback((detail: FldngHstryItem) => {
|
||||
if (!gisMaps.current) return;
|
||||
|
||||
const geometry = new Polygon([detail.areaRing]);
|
||||
@@ -4375,27 +4379,27 @@ const Map = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clearFloodHistorySelection = useCallback(() => {
|
||||
setSelectedFloodHistoryDetail(null);
|
||||
setFloodHistoryDetailPopupPosition(null);
|
||||
clearFloodHistoryLayers();
|
||||
}, [clearFloodHistoryLayers]);
|
||||
const clearFldngHstrySelection = useCallback(() => {
|
||||
setSelectedFldngHstryDetail(null);
|
||||
setFldngHstryDetailPopupPosition(null);
|
||||
clearFldngHstryLayers();
|
||||
}, [clearFldngHstryLayers]);
|
||||
|
||||
const closeFloodHistoryDetail = useCallback(() => {
|
||||
setFloodHistoryDetailPopupPosition(null);
|
||||
const closeFldngHstryDetail = useCallback(() => {
|
||||
setFldngHstryDetailPopupPosition(null);
|
||||
}, []);
|
||||
|
||||
const handleFloodHistoryRowSelect = useCallback(
|
||||
(detail: FloodHistoryItem) => {
|
||||
setSelectedFloodHistoryDetail(detail);
|
||||
renderSelectedFloodHistory(detail);
|
||||
focusFloodHistoryOnMap(detail);
|
||||
const handleFldngHstryRowSelect = useCallback(
|
||||
(detail: FldngHstryItem) => {
|
||||
setSelectedFldngHstryDetail(detail);
|
||||
renderSelectedFldngHstry(detail);
|
||||
focusFldngHstryOnMap(detail);
|
||||
setIsSearchModalOpen(false);
|
||||
},
|
||||
[focusFloodHistoryOnMap, renderSelectedFloodHistory, setIsSearchModalOpen],
|
||||
[focusFldngHstryOnMap, renderSelectedFldngHstry, setIsSearchModalOpen],
|
||||
);
|
||||
|
||||
const openFloodHistoryPopupWindow = useCallback(() => {
|
||||
const openFldngHstryPopupWindow = useCallback(() => {
|
||||
const popup = openWindowPopup(FLOOD_HISTORY_POPUP_KEY as WindowPopupKey);
|
||||
if (!popup) {
|
||||
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
|
||||
@@ -5105,16 +5109,16 @@ const Map = () => {
|
||||
}, [loadSinkholeDetail, sinkholeScope]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFloodHistoryPopupMessage = (event: MessageEvent) => {
|
||||
const handleFldngHstryPopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== FLOOD_HISTORY_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
if (event.data?.type !== FLDNG_HSTRY_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
|
||||
handleFloodHistoryRowSelect(event.data.row as FloodHistoryItem);
|
||||
handleFldngHstryRowSelect(event.data.row as FldngHstryItem);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleFloodHistoryPopupMessage);
|
||||
return () => window.removeEventListener("message", handleFloodHistoryPopupMessage);
|
||||
}, [handleFloodHistoryRowSelect]);
|
||||
window.addEventListener("message", handleFldngHstryPopupMessage);
|
||||
return () => window.removeEventListener("message", handleFldngHstryPopupMessage);
|
||||
}, [handleFldngHstryRowSelect]);
|
||||
|
||||
// WaterDepth 표시 여부를 결정하는 함수
|
||||
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
@@ -5171,10 +5175,10 @@ const Map = () => {
|
||||
WATER_DEPTH_LAYER_TYPES.includes(String(node?.data?.lyrTyNm ?? "")),
|
||||
);
|
||||
setIsAgedPipeChecked(agedPipeChecked);
|
||||
const floodHistoryChecked = checkedNodesData.some((node) => isFloodHistoryNode(node));
|
||||
setIsFloodHistoryChecked(floodHistoryChecked);
|
||||
if (!floodHistoryChecked) {
|
||||
clearFloodHistorySelection();
|
||||
const fldngHstryChecked = checkedNodesData.some((node) => isFldngHstryNode(node));
|
||||
setIsFldngHstryChecked(fldngHstryChecked);
|
||||
if (!fldngHstryChecked) {
|
||||
clearFldngHstrySelection();
|
||||
}
|
||||
const sinkholeChecked = checkedNodesData.some((node) => isSinkholeLayerNode(node));
|
||||
setIsSinkholeChecked(sinkholeChecked);
|
||||
@@ -5197,7 +5201,7 @@ const Map = () => {
|
||||
removeSinkholeLayer();
|
||||
closeSinkholeDetail();
|
||||
}
|
||||
}, [clearFloodHistorySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]);
|
||||
}, [clearFldngHstrySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]);
|
||||
|
||||
const waterDepthLegendItems = useMemo(
|
||||
() => extractWaterDepthLegendItems(checkedNodesDataRef.current ?? []),
|
||||
@@ -5228,14 +5232,14 @@ const Map = () => {
|
||||
openSinkholePopupWindow();
|
||||
return false;
|
||||
}
|
||||
if (isFloodHistoryNode(node)) {
|
||||
openFloodHistoryPopupWindow();
|
||||
if (isFldngHstryNode(node)) {
|
||||
openFldngHstryPopupWindow();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[openAgedPipeList, openFloodHistoryPopupWindow, openSinkholePopupWindow],
|
||||
[openAgedPipeList, openFldngHstryPopupWindow, openSinkholePopupWindow],
|
||||
);
|
||||
|
||||
// 현재 체크된 특수 레이어만 하단 설정영역에 모아 렌더링하기 위한 패널 목록이다.
|
||||
@@ -5409,6 +5413,43 @@ const Map = () => {
|
||||
};
|
||||
}, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID);
|
||||
|
||||
if (!isFldngHstryChecked) return;
|
||||
|
||||
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === FLOOD_HISTORY_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
) as any;
|
||||
|
||||
if (!baseLayerCode) return;
|
||||
|
||||
const layerCode = {
|
||||
...baseLayerCode,
|
||||
id: FLDNG_HSTRY_WFS_LAYER_ID,
|
||||
lyrSn: FLDNG_HSTRY_WFS_LAYER_ID,
|
||||
lyrNm: "침수흔적도",
|
||||
lyrTyNm: FLOOD_HISTORY_LAYER_TYPE,
|
||||
actlLyrYn: "Y",
|
||||
exprsYn: "Y",
|
||||
clickPsblYn: "Y",
|
||||
cmncNm: "bbox",
|
||||
srvcNm: "wfs",
|
||||
};
|
||||
|
||||
const layer = core?.addWfsLayer?.(layerCode);
|
||||
layer?.set?.("id", FLDNG_HSTRY_WFS_LAYER_ID);
|
||||
layer?.set?.("lyrNm", "침수흔적도");
|
||||
layer?.set?.("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
|
||||
|
||||
return () => {
|
||||
core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID);
|
||||
};
|
||||
}, [isFldngHstryChecked, mapReady]);
|
||||
|
||||
const openAnalysisPopup = useCallback((popupKey: WindowPopupKey) => {
|
||||
const popup = openWindowPopup(popupKey);
|
||||
if (!popup) {
|
||||
@@ -7110,25 +7151,25 @@ const Map = () => {
|
||||
}, [renderSelectedSinkhole, selectedSinkholeDetail, sinkholeRadiusMeters]);
|
||||
|
||||
useEffect(() => {
|
||||
renderSelectedFloodHistory(selectedFloodHistoryDetail);
|
||||
}, [renderSelectedFloodHistory, selectedFloodHistoryDetail]);
|
||||
renderSelectedFldngHstry(selectedFldngHstryDetail);
|
||||
}, [renderSelectedFldngHstry, selectedFldngHstryDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const clickKey = map.on("singleclick", (event: any) => {
|
||||
if (!selectedFloodHistoryDetail || !isFloodHistoryChecked) return;
|
||||
if (!selectedFldngHstryDetail || !isFldngHstryChecked) return;
|
||||
|
||||
let clickedFloodHistory = false;
|
||||
let clickedFldngHstry = false;
|
||||
map.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
(feature: any, layer: any) => {
|
||||
if (
|
||||
layer === floodHistoryAreaLayerRef.current ||
|
||||
layer === floodHistoryDepthLayerRef.current
|
||||
layer === fldngHstryAreaLayerRef.current ||
|
||||
layer === fldngHstryDepthLayerRef.current
|
||||
) {
|
||||
clickedFloodHistory = true;
|
||||
clickedFldngHstry = true;
|
||||
return feature;
|
||||
}
|
||||
return undefined;
|
||||
@@ -7136,14 +7177,14 @@ const Map = () => {
|
||||
{ hitTolerance: 4 },
|
||||
);
|
||||
|
||||
if (!clickedFloodHistory) return;
|
||||
if (!clickedFldngHstry) return;
|
||||
|
||||
const size = map.getSize?.() ?? [1200, 800];
|
||||
const popupWidth = 220;
|
||||
const popupHeight = 210;
|
||||
const nextLeft = Math.min(Math.max(event.pixel[0] + 12, 12), size[0] - popupWidth - 12);
|
||||
const nextTop = Math.min(Math.max(event.pixel[1] - 12, 12), size[1] - popupHeight - 12);
|
||||
setFloodHistoryDetailPopupPosition({
|
||||
setFldngHstryDetailPopupPosition({
|
||||
left: nextLeft,
|
||||
top: nextTop,
|
||||
});
|
||||
@@ -7153,13 +7194,13 @@ const Map = () => {
|
||||
return () => {
|
||||
unByKey(clickKey);
|
||||
};
|
||||
}, [isFloodHistoryChecked, mapReady, selectedFloodHistoryDetail, setIsSearchModalOpen]);
|
||||
}, [isFldngHstryChecked, mapReady, selectedFldngHstryDetail, setIsSearchModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearFloodHistoryLayers();
|
||||
clearFldngHstryLayers();
|
||||
};
|
||||
}, [clearFloodHistoryLayers]);
|
||||
}, [clearFldngHstryLayers]);
|
||||
|
||||
|
||||
return (
|
||||
@@ -7287,11 +7328,11 @@ const Map = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedFloodHistoryDetail && floodHistoryDetailPopupPosition ? (
|
||||
<FloodHistoryDetailPanel
|
||||
detail={selectedFloodHistoryDetail}
|
||||
position={floodHistoryDetailPopupPosition}
|
||||
onClose={closeFloodHistoryDetail}
|
||||
{selectedFldngHstryDetail && fldngHstryDetailPopupPosition ? (
|
||||
<FldngHstryDetailPanel
|
||||
detail={selectedFldngHstryDetail}
|
||||
position={fldngHstryDetailPopupPosition}
|
||||
onClose={closeFldngHstryDetail}
|
||||
/>
|
||||
) : null}
|
||||
<AnalysisPopupMenu
|
||||
@@ -7375,7 +7416,7 @@ const Map = () => {
|
||||
treeTogglePosition="center"
|
||||
zoomGuide={zoomGuide}
|
||||
layerIconVisible={(node: any) =>
|
||||
isAgedPipeLayerNode(node) || isSinkholeRootNode(node) || isFloodHistoryNode(node)
|
||||
isAgedPipeLayerNode(node) || isSinkholeRootNode(node) || isFldngHstryNode(node)
|
||||
}
|
||||
|
||||
defaultZoom={7.5}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Label } from '@common/components';
|
||||
import SwagePpl from './swagePpl';
|
||||
import SwagePrcsFclt from './swagePrcsFclt';
|
||||
import SwagePrcsZone from './swagePrcsZone';
|
||||
import FldngWtlvMap from './fldngWtlvMap';
|
||||
import SwageMhl from './swageMhl';
|
||||
import SwagePmstn from './swagePmstn';
|
||||
import Stofcbr from './stofcbr';
|
||||
@@ -25,6 +26,7 @@ export const BASIC_TYPES = [
|
||||
{ val: "tb_ms_swage_ppl", text: "하수관로" },
|
||||
{ val: "tb_il_swage_prcs_fclt", text: "하수처리시설" },
|
||||
{ val: "tb_ms_swage_prcs_zone", text: "하수처리구역" },
|
||||
{ val: "tb_ms_fldng_wtlv_map", text: "침수흔적도" },
|
||||
{ val: "tb_ms_swage_mhl", text: "하수맨홀" },
|
||||
{ val: "tb_ms_swage_pmstn", text: "하수펌프장" },
|
||||
{ val: "tb_ms_stofcbr", text: "우수토실" },
|
||||
@@ -56,6 +58,7 @@ const BasicTab: React.FC = () => {
|
||||
{basicType === "tb_ms_swage_ppl" && (<SwagePpl />)} {/* 하수관로 */}
|
||||
{basicType === "tb_il_swage_prcs_fclt" && (<SwagePrcsFclt />)} {/* 하수처리시설 */}
|
||||
{basicType === "tb_ms_swage_prcs_zone" && (<SwagePrcsZone />)} {/* 하수처리구역 */}
|
||||
{basicType === "tb_ms_fldng_wtlv_map" && (<FldngWtlvMap />)} {/* 침수흔적도 */}
|
||||
{basicType === "tb_ms_swage_mhl" && (<SwageMhl />)} {/* 하수맨홀 */}
|
||||
{basicType === "tb_ms_swage_pmstn" && (<SwagePmstn />)} {/* 하수펌프장 */}
|
||||
{basicType === "tb_ms_stofcbr" && (<Stofcbr />)} {/* 우수토실 */}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '../../../store/useSearchModalStore';
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
// 침수흔적도용 CQL 필터 생성
|
||||
export const fldngWtlvMapCqlFilters = () => {
|
||||
const { ctpvCd, sggCd, var1, var2 } = useSearchModalStore.getState();
|
||||
const cqlFilters: string[] = [];
|
||||
if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`stdg_cd LIKE '${ctpvCd.trim()}%'`);
|
||||
if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`stdg_cd LIKE '${sggCd.trim()}%'`);
|
||||
if (var1) cqlFilters.push(`fldng_yr = '${var1.trim()}'`);
|
||||
if (var2) cqlFilters.push(`fldng_dstr_nm ILIKE '%${var2.trim()}%'`);
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const FldngWtlvMap: React.FC = () => {
|
||||
const {
|
||||
var1, setVar1, var2, setVar2,
|
||||
ctpvCd, setCtpvCd,
|
||||
sggCd, setSggCd,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-2 flex items-center">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
<Label className="mr-2 whitespace-nowrap text-xl font-medium text-gray-600">침수년도</Label>
|
||||
<Input
|
||||
className="mr-8 w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="YYYY"
|
||||
value={var1}
|
||||
onChange={(e) => setVar1(e.target.value.replace(/[^0-9]/g, '').slice(0, 4))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label className="mr-2 whitespace-nowrap text-xl font-medium text-gray-600">침수재해명</Label>
|
||||
<Input
|
||||
className='!text-xl w-[180px] placeholder:text-center mr-8'
|
||||
placeholder="입력"
|
||||
value={var2}
|
||||
onChange={(e) => setVar2(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-1 justify-end">
|
||||
<Button className="py-2 text-base" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FldngWtlvMap);
|
||||
@@ -18,7 +18,7 @@ import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import { ExclnSewagePumpCol2, FldngWtlvMapCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
@@ -57,6 +57,13 @@ const BASIC_SORT_KEYS: Record<string, Record<string, string>> = {
|
||||
swtrmtZoneNm: 'swtrmt_zone_nm', ctpvNm: 'ctpv_nm', sggNm: 'sgg_nm',
|
||||
swtrmtFcltNm: 'swtrmt_fclt_nm',
|
||||
},
|
||||
tb_ms_fldng_wtlv_map: {
|
||||
rowNumber: 'rowNumber',
|
||||
fldngYr: 'fldng_yr', fldngDstrNm: 'fldng_dstr_nm', ctpvNm: 'stdg_cd', sggNm: 'stdg_cd',
|
||||
bgngYmd: 'bgng_ymd', fldngBgngHm: 'fldng_bgng_hm', endYmd: 'end_ymd', fldngEndHm: 'fldng_end_hm',
|
||||
fldngHr: 'fldng_hr', fldngDstrctNm: 'fldng_dstrct_nm', fldngCsDtlCd: 'fldng_cs_dtl_cd',
|
||||
avgFldngWtlv: 'avg_fldng_wtlv', fldngArea: 'fldng_area', exmnYr: 'exmn_yr', damAmt: 'dam_amt',
|
||||
},
|
||||
tb_ms_swage_mhl: {
|
||||
rowNumber: 'rowNumber',
|
||||
fcltMngNo: 'fclt_mng_no', instlYmd: 'instl_ymd', swageMhlDiamVl: 'swage_mhl_diam_vl',
|
||||
@@ -138,9 +145,10 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = 'launcher', auto
|
||||
|
||||
const dynamicHeadlist: UiHeadProps[] = useMemo(() => {
|
||||
if (activeTab === 'basic') {
|
||||
const headlist = basicType === 'tb_ms_swage_ppl' ? MsSwagePplCol2
|
||||
const headlist = basicType === 'tb_ms_swage_ppl' ? MsSwagePplCol2
|
||||
: basicType === 'tb_il_swage_prcs_fclt' ? SwtrmtFcltCol2
|
||||
: basicType === 'tb_ms_swage_prcs_zone' ? SwtrmtPrcsZoneCol2
|
||||
: basicType === 'tb_ms_fldng_wtlv_map' ? FldngWtlvMapCol2
|
||||
: basicType === 'tb_ms_swage_mhl' ? MsSwageMhlCol2
|
||||
: basicType === 'tb_ms_swage_pmstn' ? ExclnSewagePumpCol2
|
||||
: basicType === 'tb_ms_stofcbr' ? stofcbrCol2 : [];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
|
||||
import {
|
||||
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
|
||||
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
|
||||
FldngWtlvMapExcelCol, MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
|
||||
} from '../components/common/table';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { BASIC_TYPES } from './basicTab';
|
||||
@@ -28,6 +28,7 @@ import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { swagePplCqlFilters } from './swagePpl';
|
||||
import { swagePrcsFcltCqlFilters } from './swagePrcsFclt';
|
||||
import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
|
||||
import { fldngWtlvMapCqlFilters } from './fldngWtlvMap';
|
||||
import { swageMhlCqlFilters } from './swageMhl';
|
||||
import { swagePmstnCqlFilters } from './swagePmstn';
|
||||
import { stofcbrCqlFilters } from './stofcbr';
|
||||
@@ -79,6 +80,7 @@ export default function useBasicTab() {
|
||||
if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters(); // 하수관로
|
||||
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설
|
||||
else if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역
|
||||
else if (basicType === 'tb_ms_fldng_wtlv_map') cqlFilters = fldngWtlvMapCqlFilters(); // 침수흔적도
|
||||
else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters(); // 하수맨홀
|
||||
else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters(); // 하수펌프장
|
||||
else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters(); // 우수토실
|
||||
@@ -96,7 +98,7 @@ export default function useBasicTab() {
|
||||
let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? '';
|
||||
|
||||
if (!ctpvCd || !sggCd) {
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? item?.stdgCd ?? formattedItem?.stdgCd ?? '')?.trim();
|
||||
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
|
||||
if (!sggCd && code) sggCd = code.slice(0, 5);
|
||||
}
|
||||
@@ -214,6 +216,13 @@ export default function useBasicTab() {
|
||||
headerRange = "A1:D1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_fldng_wtlv_map': {
|
||||
// 침수흔적도
|
||||
cqlFilters = fldngWtlvMapCqlFilters();
|
||||
headerMapping = FldngWtlvMapExcelCol;
|
||||
headerRange = "A1:O1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
// 하수맨홀
|
||||
cqlFilters = swageMhlCqlFilters();
|
||||
@@ -255,7 +264,7 @@ export default function useBasicTab() {
|
||||
let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? '';
|
||||
|
||||
if (!ctpvCd || !sggCd) {
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? item?.stdgCd ?? formattedItem?.stdgCd ?? '')?.trim();
|
||||
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
|
||||
if (!sggCd && code) sggCd = code.slice(0, 5);
|
||||
}
|
||||
@@ -311,6 +320,26 @@ export default function useBasicTab() {
|
||||
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-'
|
||||
};
|
||||
}
|
||||
case 'tb_ms_fldng_wtlv_map': {
|
||||
// 침수흔적도
|
||||
return {
|
||||
fldngYr: formattedItem.fldngYr ?? item.fldngYr ?? '-',
|
||||
fldngDstrNm: formattedItem.fldngDstrNm ?? item.fldngDstrNm ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
|
||||
bgngYmd: formattedItem.bgngYmd ?? item.bgngYmd ?? '-',
|
||||
fldngBgngHm: formattedItem.fldngBgngHm ?? item.fldngBgngHm ?? '-',
|
||||
endYmd: formattedItem.endYmd ?? item.endYmd ?? '-',
|
||||
fldngEndHm: formattedItem.fldngEndHm ?? item.fldngEndHm ?? '-',
|
||||
fldngHr: formattedItem.fldngHr ?? item.fldngHr ?? '-',
|
||||
fldngDstrctNm: formattedItem.fldngDstrctNm ?? item.fldngDstrctNm ?? '-',
|
||||
fldngCsDtlCd: formattedItem.fldngCsDtlCd ?? item.fldngCsDtlCd ?? '-',
|
||||
avgFldngWtlv: formattedItem.avgFldngWtlv ?? item.avgFldngWtlv ?? '-',
|
||||
fldngArea: formattedItem.fldngArea ?? item.fldngArea ?? '-',
|
||||
exmnYr: formattedItem.exmnYr ?? item.exmnYr ?? '-',
|
||||
damAmt: formattedItem.damAmt ?? item.damAmt ?? '-',
|
||||
};
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
// 하수맨홀
|
||||
return {
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
| popup 정의 | `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts` | 진행 | tree 목록 진입 popup 키/크기 정의 |
|
||||
| popup 등록 | `FE_APP_ROOT/src/features/windowPopup/registry.tsx` | 진행 | placeholder에서 실제 popup content로 교체 |
|
||||
| 분석도구 메뉴 | `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx` | 진행 | 분석도구에서 `침수발생이력` 제거 |
|
||||
| popup content | `FE_APP_ROOT/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx` | 진행 | popup 본문 연결 |
|
||||
| 신규 popup 페이지 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx` | 진행 | 조회조건/목록/다운로드 |
|
||||
| 신규 상세패널 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx` | 진행 | 기본정보/발생일시/원인·피해 탭 |
|
||||
| 신규 타입/샘플데이터 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts` | 진행 | mock 데이터, postMessage 타입, legend 정의 |
|
||||
| popup content | `FE_APP_ROOT/src/features/windowPopup/ui/FldngHstryPopupContent.tsx` | 진행 | popup 본문 연결 |
|
||||
| 신규 popup 페이지 | `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx` | 진행 | 조회조건/목록/다운로드 |
|
||||
| 신규 상세패널 | `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx` | 진행 | 기본정보/발생일시/원인·피해 탭 |
|
||||
| 신규 타입/샘플데이터 | `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts` | 진행 | mock 데이터, postMessage 타입, legend 정의 |
|
||||
|
||||
## 4. 구현 범위
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/registry.tsx`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts`
|
||||
- `FE_APP_ROOT/src/features/windowPopup/ui/FldngHstryPopupContent.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx`
|
||||
- `FE_APP_ROOT/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts`
|
||||
- 구현 내용
|
||||
- 분석도구의 `침수발생이력` 항목은 제거하고, 레이어 트리 `침수흔적도` 목록 진입으로 popup을 열도록 바꿨다.
|
||||
- `침수발생이력` popup placeholder를 실제 popup 페이지로 교체했다.
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- FE 변경분 기준 `node node_modules\typescript\bin\tsc --noEmit --ignoreDeprecations 5.0 -p tsconfig.json`을 실행했다.
|
||||
- 이번 변경으로 새로 생긴 오류는 없었고, 아래 선행 타입 오류 때문에 전체 통과는 하지 못했다.
|
||||
- `app/(map-layout)/map/components/common/index.ts`
|
||||
- `app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts`
|
||||
- `app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts`
|
||||
- `app/(map-layout)/map/page.tsx`
|
||||
- `app/html/(map-layout)/map/components/common/index.ts`
|
||||
- `app/html/(with-base-layout)/layout.tsx`
|
||||
|
||||
@@ -71,6 +71,7 @@ export function middleware(req: NextRequest) {
|
||||
if (
|
||||
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|
||||
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|
||||
|| typename == "hasudo:tb_ms_fldng_wtlv_map" //침수흔적도
|
||||
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|
||||
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
|
||||
) {
|
||||
|
||||
@@ -141,8 +141,8 @@ export const windowPopupDefinitions = {
|
||||
width: 1080,
|
||||
height: 760,
|
||||
},
|
||||
floodHistoryPopup: {
|
||||
key: "floodHistoryPopup",
|
||||
fldngHstryPopup: {
|
||||
key: "fldngHstryPopup",
|
||||
title: "침수발생이력",
|
||||
width: 1160,
|
||||
height: 820,
|
||||
|
||||
@@ -19,7 +19,7 @@ import SinkholePopupContent from "./ui/SinkholePopupContent";
|
||||
import SscDetailPopupContent from "./ui/SscDetailPopupContent";
|
||||
import type { WindowPopupContentProps } from "./model/types";
|
||||
import FeaturePlaceholderPopupContent from "./ui/FeaturePlaceholderPopupContent";
|
||||
import FloodHistoryPopupContent from "./ui/FloodHistoryPopupContent";
|
||||
import FldngHstryPopupContent from "./ui/FldngHstryPopupContent";
|
||||
import GisEditSessionPopupContent from "./ui/GisEditSessionPopupContent";
|
||||
import GisManagementPopupContent from "./ui/GisManagementPopupContent";
|
||||
import PipeAnalysisPopupContent from "./ui/PipeAnalysisPopupContent";
|
||||
@@ -117,9 +117,9 @@ export const windowPopupRegistry = {
|
||||
...windowPopupDefinitions.floodPredictionMapPopup,
|
||||
component: FloodPredictionPlaceholderPopupContent,
|
||||
},
|
||||
floodHistoryPopup: {
|
||||
...windowPopupDefinitions.floodHistoryPopup,
|
||||
component: FloodHistoryPopupContent,
|
||||
fldngHstryPopup: {
|
||||
...windowPopupDefinitions.fldngHstryPopup,
|
||||
component: FldngHstryPopupContent,
|
||||
},
|
||||
gisEditPopup: {
|
||||
...windowPopupDefinitions.gisEditPopup,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import FldngHstryPopupPage from "@/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage";
|
||||
|
||||
export default function FldngHstryPopupContent() {
|
||||
return <FldngHstryPopupPage />;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import FloodHistoryPopupPage from "@/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage";
|
||||
|
||||
export default function FloodHistoryPopupContent() {
|
||||
return <FloodHistoryPopupPage />;
|
||||
}
|
||||
Reference in New Issue
Block a user