refactor(sn): modularize map and features and standardize file naming
This commit is contained in:
@@ -19,7 +19,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { NoFooterLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import useStore from '../store/useStore';
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { authService } from "@/entities/auth";
|
||||
|
||||
export default function MapLayout({
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@
|
||||
"use client";
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import {
|
||||
getAgedPipeStatusByCtpvValidationMessage,
|
||||
normalizeAgedPipeStatusByCtpvFilters,
|
||||
type AgedPipeStatusByCtpvFilters,
|
||||
type AgedPipeStatusByCtpvResponse,
|
||||
} from "./agedPipeStatusByCtpvUtils";
|
||||
} from "./aged-pipe-status-by-ctpv-utils";
|
||||
|
||||
/** 적용조건의 시도별 노후관로현황 전체를 조회해 연도 그룹 Excel로 생성한다. */
|
||||
export const downloadAgedPipeStatusByCtpvExcel = async (
|
||||
+6
-6
@@ -18,10 +18,10 @@ import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import {
|
||||
AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
|
||||
buildAgedPipeStatusYearOptions,
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
type AgedPipeStatusByCtpvFilters,
|
||||
type AgedPipeStatusByCtpvResponse,
|
||||
type AgedPipeStatusByCtpvRow,
|
||||
} from "./agedPipeStatusByCtpvUtils";
|
||||
import { downloadAgedPipeStatusByCtpvExcel } from "./agedPipeStatusByCtpvExport";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
} from "./aged-pipe-status-by-ctpv-utils";
|
||||
import { downloadAgedPipeStatusByCtpvExcel } from "./aged-pipe-status-by-ctpv-export";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
|
||||
// CS 목록과 동일하게 한 페이지에는 최대 10개 시도만 표시한다.
|
||||
const AGED_PIPE_STATUS_PAGE_SIZE = 10;
|
||||
+3
-7
@@ -1,13 +1,9 @@
|
||||
/** 노후관로 목록의 엑셀 다운로드 데이터와 서식을 생성한다. */
|
||||
"use client";
|
||||
|
||||
import { postBlobData } from "@/components/lib/api";
|
||||
import { fileDownload } from "@/components/lib/utils/fileDownload";
|
||||
import {
|
||||
AGED_PIPE_EXCEL_FIELDS,
|
||||
normalizeAgedPipeFilters,
|
||||
type AgedPipeFilters,
|
||||
} from "./agedPipeUtils";
|
||||
import { postBlobData } from "@/shared/api/file-api";
|
||||
import { fileDownload } from "@/components/lib/utils/file-download";
|
||||
import { AGED_PIPE_EXCEL_FIELDS, normalizeAgedPipeFilters, type AgedPipeFilters } from "@/entities/facility/lib/aged-pipe-utils";
|
||||
|
||||
type AgedPipeExportProgress = {
|
||||
fileName?: string;
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { validateElapsedYears, type AgedPipeFilters } from "./agedPipeUtils";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
import { validateElapsedYears, type AgedPipeFilters } from "@/entities/facility/lib/aged-pipe-utils";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
type MaybePromise<T = void> = T | Promise<T>;
|
||||
|
||||
+26
-17
@@ -3,15 +3,16 @@
|
||||
* 노후관로를 검색하고 목록·엑셀 다운로드·지도 위치 확인 기능을 제공한다.
|
||||
*/
|
||||
"use client";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
@@ -25,16 +26,17 @@ import {
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
type AgedPipeManagementAgency,
|
||||
} from "./agedPipeUtils";
|
||||
import { downloadAgedPipeExcel } from "./agedPipeExport";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
} from "@/entities/facility/lib/aged-pipe-utils";
|
||||
import { downloadAgedPipeExcel } from "./aged-pipe-export";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
|
||||
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
export default function AgedPipePopupPage() {
|
||||
const facilityCodes = useFacilityCodeCatalog();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const searchParams = useSearchParams();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
@@ -56,12 +58,15 @@ export default function AgedPipePopupPage() {
|
||||
useState<AgedPipeFilters>(initialFilters);
|
||||
const [rows, setRows] = useState<AgedPipeRow[]>([]);
|
||||
// 관리기관명 조회가 늦게 완료되어도 기존 목록을 재조회하지 않고 표시만 갱신한다.
|
||||
const [managementAgencies, setManagementAgencies] = useState<AgedPipeManagementAgency[]>([]);
|
||||
const [managementAgencies, setManagementAgencies] = useState<
|
||||
AgedPipeManagementAgency[]
|
||||
>([]);
|
||||
const displayRows = useMemo(
|
||||
() => rows.map((row) => ({
|
||||
...row,
|
||||
mngInstNm: getAgedPipeManagementName(row, managementAgencies),
|
||||
})),
|
||||
() =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
mngInstNm: getAgedPipeManagementName(row, managementAgencies),
|
||||
})),
|
||||
[rows, managementAgencies],
|
||||
);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -89,12 +94,16 @@ export default function AgedPipePopupPage() {
|
||||
// 기존 GIS 관리 화면과 같은 기관 목록을 사용하고, 실패 시 관로 조회는 유지한다.
|
||||
getData("v1/fcltPpl/mngInsts")
|
||||
.then((response) => {
|
||||
if (active) setManagementAgencies(Array.isArray(response) ? response : []);
|
||||
if (active)
|
||||
setManagementAgencies(Array.isArray(response) ? response : []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) alert("관리기관명을 불러오지 못해 관리기관 코드로 표시합니다.");
|
||||
if (active)
|
||||
alert("관리기관명을 불러오지 못해 관리기관 코드로 표시합니다.");
|
||||
});
|
||||
return () => { active = false; };
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [alert]);
|
||||
|
||||
const fetchRows = useCallback(
|
||||
@@ -116,12 +125,12 @@ export default function AgedPipePopupPage() {
|
||||
const total = Number(response?.count ?? 0);
|
||||
const rows = responseRows.map(
|
||||
(item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg, facilityCodes),
|
||||
);
|
||||
|
||||
return { rows, total };
|
||||
},
|
||||
[ctpv, sgg],
|
||||
[ctpv, sgg, facilityCodes],
|
||||
);
|
||||
|
||||
const loadRows = useCallback(
|
||||
+3
-3
@@ -17,13 +17,13 @@ import {
|
||||
Waves,
|
||||
} from "lucide-react";
|
||||
|
||||
import { createPredictionAnalysisTimeRange } from "@/features/fldngPredcPrst/model/analysisTimeRange";
|
||||
import { createPredictionAnalysisTimeRange } from "@/features/fldng-predc-prst/model/analysis-time-range";
|
||||
import {
|
||||
windowPopupDefinitions,
|
||||
type WindowPopupKey,
|
||||
} from "@/features/windowPopup/model/popupDefinitions";
|
||||
} from "@/features/window-popup/model/popup-definitions";
|
||||
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
|
||||
type Props = {
|
||||
onOpenPopup: (popupKey: WindowPopupKey) => Window | null;
|
||||
@@ -1,72 +0,0 @@
|
||||
/** 선택한 지도 레이어 종류에 맞는 시설 상세 컴포넌트를 반환한다. */
|
||||
import MsSwagePpl from "../popupComponent/MsSwagePpl"; // 하수관로
|
||||
import sewagePump from "../popupComponent/sewagePump"; // 하수펌프
|
||||
import smh from "../popupComponent/smh";
|
||||
import Ssc from "../popupComponent/Ssc";
|
||||
import StormWaterPump from "../popupComponent/stormWaterPump";
|
||||
import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
|
||||
import SwtrmtPrcsZoneAnalysis from "../popupComponent/SwtrmtPrcsZoneAnalysis";
|
||||
|
||||
type LayerComponentProps = {
|
||||
featureData: any;
|
||||
isMainSsc?: boolean;
|
||||
isDrtwSewagePump?: boolean;
|
||||
rainMeasureData?: any;
|
||||
onSaveClick?: () => void;
|
||||
onFormDataChange?: (data: any) => void; // 추가
|
||||
chmbrXcnts: string;
|
||||
chmbrYdnts: string;
|
||||
gisObject?: Record<string, any>; // GIS 객체 추가
|
||||
rawFeatureData?: any;
|
||||
surveyResultContent?: React.ReactNode;
|
||||
};
|
||||
type LayerContentMap = {
|
||||
[key: string]: {
|
||||
component?: React.ComponentType<any>;
|
||||
props?: any;
|
||||
};
|
||||
};
|
||||
// 레이어 이름과 컴포넌트를 매핑
|
||||
// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업
|
||||
const layerContentMap: LayerContentMap = {
|
||||
"하수처리시설": { component: SwtrmtFclt, }, // 디지털 자료관 연계정보가 없으니 일단 주석 처리
|
||||
"하수처리구역": { component: SwtrmtPrcsZoneAnalysis },
|
||||
"우수토실": { component: Ssc, },
|
||||
"중점우수토실": { component: Ssc, }, // 중점우수토실 여부 설정
|
||||
"하수관로": { component: MsSwagePpl, },
|
||||
"하수맨홀": { component: smh, },
|
||||
"하수펌프장": { component: sewagePump },
|
||||
// "빗물펌프": { component: StormWaterPump },
|
||||
// "오수펌프": { component: sewagePump },
|
||||
};
|
||||
|
||||
// 동적 컴포넌트 반환 함수
|
||||
export const getLayerContentComponent = (layerNm: string): React.FC<LayerComponentProps> | null => {
|
||||
const component = layerContentMap[layerNm];
|
||||
if (!component || !component.component) {
|
||||
// component가 없으면 null 반환
|
||||
return () => null;
|
||||
}
|
||||
return component.component as React.FC<LayerComponentProps>
|
||||
};
|
||||
// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단)
|
||||
export const handleInputChange = (section: string,
|
||||
index: number,
|
||||
field: string,
|
||||
value: string,
|
||||
dataMap: {
|
||||
[key: string]: {
|
||||
data: { year: string; type: string, bizInfoNm: string, bizType: string }[];
|
||||
setData: React.Dispatch<React.SetStateAction<{ year: string; type: string, bizInfoNm: string, bizType: string }[]>>;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const target = dataMap[section];
|
||||
if (!target) return;
|
||||
|
||||
const updated = [...target.data];
|
||||
if (updated[index]) {
|
||||
updated[index][field as keyof typeof updated[number]] = value;
|
||||
}
|
||||
target.setData(updated);
|
||||
};
|
||||
@@ -1,933 +0,0 @@
|
||||
/** 지도 시설 상세 및 목록에서 사용하는 컬럼·코드 정의를 제공한다. */
|
||||
// 하수관로
|
||||
export const MsSwagePplCol = [
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
{ id: 'mngInstNm', title: '관리기관' },
|
||||
{ id: 'inst_mng_no', title: '기관관리번호' },
|
||||
{ id: 'flowDirectionBasis', title: '유하방향 근거' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'elapsedYears', title: '경과년도' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
{ id: 'pdmt_vl', title: '관경(mm)' },
|
||||
{ id: 'prlg_vl', title: '연장(m)' },
|
||||
{ id: 'usg_cd', title: '용도' },
|
||||
{ id: 'mqlt_cd', title: '재질' },
|
||||
{ id: 'avg_grdn', title: '평균구배(m/m)' },
|
||||
{ id: 'drtw_mltv_area', title: '오수배수면적(m²)' },
|
||||
{ id: 'excln_mltv_area', title: '우수배수면적(m²)' },
|
||||
{ id: 'tmpt_dpth', title: '시점깊이(m)' },
|
||||
{ id: 'tmpt_mhl_mng_no', title: '시점맨홀관리번호' },
|
||||
{ id: 'scend_dpth', title: '종점깊이(m)' },
|
||||
{ id: 'scend_mhl_mng_no', title: '종점맨홀관리번호' },
|
||||
{ id: 'trfln_cnt', title: '차선수' },
|
||||
{ id: 'vrtc_len', title: '세로길이(m)' },
|
||||
{ id: 'wdth_len', title: '가로길이(m)' },
|
||||
// { id: 'rnyssn_fvlc', title: '우기 시 유속(m/s)' },
|
||||
// { id: 'clar_fvlc', title: '건기 시 유속(m/s)' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
// { id: 'lyrTypeNm', title: '레이어타입명' },
|
||||
]
|
||||
// 하수관로 searchModal 기본정보tab
|
||||
export const MsSwagePplCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
{ id: 'fcltMngNo', title: '시설\n관리번호' },
|
||||
{ id: 'instMngNo', title: '기관\n관리번호' },
|
||||
{ id: 'flowDirectionBasis', title: '유하방향\n근거' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: "swtrmt_zone_nm", title: "처리구역" },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'usgCd', title: '용도' },
|
||||
{ id: 'pdmtVl', title: '관경\n(mm)' },
|
||||
{ id: 'prlgVl', title: '연장\n(m)' },
|
||||
// { id: 'avgGrdn', title: '평균구배\n(m/m)' },
|
||||
{ id: 'tmptDpth', title: '시점깊이\n(m)' },
|
||||
{ id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' },
|
||||
{ id: 'scendDpth', title: '종점깊이\n(m)' },
|
||||
{ id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' },
|
||||
{ id: 'vrtcLen', title: '세로길이\n(m)' },
|
||||
{ id: 'wdthLen', title: '가로길이\n(m)' },
|
||||
{ id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'mqltCd', title: '재질코드' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
// { id: 'lyrTypeNm', title: '레이어타입명' },
|
||||
]
|
||||
export const MsSwagePplExcelCol = {
|
||||
'fcltMngNo': '시설 관리번호',
|
||||
'instMngNo': '기관 관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swtrmt_zone_nm': '처리구역',
|
||||
'instlYmd': '설치일자',
|
||||
'usgCd': '용도',
|
||||
'pdmtVl': '관경(mm)',
|
||||
'prlgVl': '연장(m)',
|
||||
'tmptDpth': '시점 깊이(m)',
|
||||
'tmptMhlMngNo': '시점맨홀 관리번호',
|
||||
'scendDpth': '종점 깊이(m)',
|
||||
'scendMhlMngNo': '종점맨홀 관리번호',
|
||||
'vrtcLen': '세로길이(m)',
|
||||
'wdthLen': '가로길이(m)',
|
||||
'fcltyShpCd': '시설물형태코드',
|
||||
'mqltCd': '재질코드',
|
||||
};
|
||||
// 하수관로 용도 코드표
|
||||
export const MS_SWAGE_PPL_USE_CODES = [
|
||||
{ val: "SBA000", text: "미분류" },
|
||||
{ val: "SBA001", text: "합류관" },
|
||||
{ val: "SBA002", text: "차집관" },
|
||||
{ val: "SBA003", text: "우수관" },
|
||||
{ val: "SBA004", text: "오수관" },
|
||||
{ val: "SBA999", text: "기타" },
|
||||
];
|
||||
// 하수관로 재질 코드표
|
||||
export const MS_SWAGE_PPL_CODES = [
|
||||
{ val: "MOP000", text: "미분류" },
|
||||
{ val: "MOP001", text: "닥타일주철관(DCIP)" },
|
||||
{ val: "MOP002", text: "강관(SP)" },
|
||||
{ val: "MOP003", text: "아연도강관(GS)" },
|
||||
{ val: "MOP004", text: "염화비닐관(PVC)" },
|
||||
{ val: "MOP005", text: "폴리에틸렌관(PE)" },
|
||||
{ val: "MOP006", text: "스테인레스관(SSP)" },
|
||||
{ val: "MOP007", text: "동관(COP)" },
|
||||
{ val: "MOP008", text: "P.S.콘크리트관(PC)" },
|
||||
{ val: "MOP009", text: "석면시멘트관(AS)" },
|
||||
{ val: "MOP010", text: "회주철관(CIP)" },
|
||||
{ val: "MOP011", text: "연관(LP)" },
|
||||
{ val: "MOP012", text: "흉관(HP)" },
|
||||
{ val: "MOP013", text: "철근콘크리트관(RC)" },
|
||||
{ val: "MOP014", text: "강관(ST)" },
|
||||
{ val: "MOP015", text: "주철관(CI)" },
|
||||
{ val: "MOP016", text: "화학제품류관(PV)" },
|
||||
{ val: "MOP017", text: "도관(VC)" },
|
||||
{ val: "MOP018", text: "석축(SE)" },
|
||||
{ val: "MOP019", text: "도복장 강관(SP)" },
|
||||
{ val: "MOP020", text: "흉관(EP)" },
|
||||
{ val: "MOP021", text: "아연도강관(GP)" },
|
||||
{ val: "MOP022", text: "청동관(BR)" },
|
||||
{ val: "MOP023", text: "고무몰딩(RM)" },
|
||||
{ val: "MOP024", text: "주강(CS)" },
|
||||
{ val: "MOP025", text: "VR관" },
|
||||
{ val: "MOP026", text: "HDPE관" },
|
||||
{ val: "MOP027", text: "시멘트라이닝주철관(C)" },
|
||||
{ val: "MOP028", text: "수도용 PE관(PE)" },
|
||||
{ val: "MOP029", text: "석면시멘트관(AC)" },
|
||||
{ val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" },
|
||||
{ val: "MOP031", text: "에폭시코팅강관(EPS)" },
|
||||
{ val: "MOP032", text: "HI-3P" },
|
||||
{ val: "MOP033", text: "철근콘크리트박스(RCB)" },
|
||||
{ val: "MOP034", text: "콘크리트중선함(CD)" },
|
||||
{ val: "MOP035", text: "후랙시불전선함(FP)" },
|
||||
{ val: "MOP036", text: "전선함(FD)" },
|
||||
{ val: "MOP038", text: "이중보온관" },
|
||||
{ val: "MOP039", text: "폴리에틸렌피복강관(PLP)" },
|
||||
{ val: "MOP041", text: "FRP" },
|
||||
{ val: "MOP042", text: "강재" },
|
||||
{ val: "MOP999", text: "기타" },
|
||||
{ val: "MOP800", text: "2중벽PE관" },
|
||||
{ val: "MOP801", text: "3중벽PE관" },
|
||||
{ val: "MOP802", text: "가스배관용 강관" },
|
||||
{ val: "MOP803", text: "강관(피복 Double, 용접접합)" },
|
||||
{ val: "MOP804", text: "강관(피복 Single, 용접접합)" },
|
||||
{ val: "MOP805", text: "강관(CP)" },
|
||||
{ val: "MOP806", text: "강철" },
|
||||
{ val: "MOP807", text: "강화플라스틱복합관" },
|
||||
{ val: "MOP808", text: "경질염화비닐관(HI-VP)" },
|
||||
{ val: "MOP809", text: "고강성PVC이중벽관" },
|
||||
{ val: "MOP810", text: "고광도PVC이중벽관" },
|
||||
{ val: "MOP811", text: "고밀도폴리에틸렌관(THP)" },
|
||||
{ val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" },
|
||||
{ val: "MOP813", text: "공장제 박스관거" },
|
||||
{ val: "MOP814", text: "관로없음(알케이블)" },
|
||||
{ val: "MOP815", text: "나선형강관" },
|
||||
{ val: "MOP816", text: "내충격수도관" },
|
||||
{ val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" },
|
||||
{ val: "MOP818", text: "내충격HI-VG2관" },
|
||||
{ val: "MOP819", text: "내충격PVC하수관" },
|
||||
{ val: "MOP820", text: "녹석(돌산호과 일종)" },
|
||||
{ val: "MOP821", text: "동관(구리관, 용접)" },
|
||||
{ val: "MOP822", text: "둥근잔돌" },
|
||||
{ val: "MOP823", text: "무근콘크리트" },
|
||||
{ val: "MOP824", text: "백관" },
|
||||
{ val: "MOP825", text: "복합철판파형관" },
|
||||
{ val: "MOP826", text: "분체라이닝관" },
|
||||
{ val: "MOP827", text: "석축박스" },
|
||||
{ val: "MOP828", text: "석축철콘" },
|
||||
{ val: "MOP829", text: "수도용경질염화비관" },
|
||||
{ val: "MOP830", text: "수도용경질염화비닐이음관" },
|
||||
{ val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" },
|
||||
{ val: "MOP832", text: "수지파형강관(PCS)" },
|
||||
{ val: "MOP833", text: "수지CSP" },
|
||||
{ val: "MOP834", text: "스테인레스 주름관" },
|
||||
{ val: "MOP835", text: "스테인레스관(STS)" },
|
||||
{ val: "MOP836", text: "스테인레스스틸파이프관" },
|
||||
{ val: "MOP837", text: "아스콘" },
|
||||
{ val: "MOP838", text: "아스팔트" },
|
||||
{ val: "MOP839", text: "아스팔트도복장강관(ASP)" },
|
||||
{ val: "MOP840", text: "아연" },
|
||||
{ val: "MOP841", text: "아연도금(백관)" },
|
||||
{ val: "MOP842", text: "알루미늄" },
|
||||
{ val: "MOP843", text: "알루미늄합금" },
|
||||
{ val: "MOP844", text: "압력배관용 탄소강관" },
|
||||
{ val: "MOP845", text: "양철관" },
|
||||
{ val: "MOP846", text: "양철주름관(CSP)" },
|
||||
{ val: "MOP847", text: "연합인슈강판" },
|
||||
{ val: "MOP848", text: "와사관(GSP)" },
|
||||
{ val: "MOP849", text: "원심력철근콘크리트" },
|
||||
{ val: "MOP850", text: "유리섬유관(GRP)" },
|
||||
{ val: "MOP851", text: "유리섬유복합관(GRP)" },
|
||||
{ val: "MOP852", text: "이음매없는동및동합금관" },
|
||||
{ val: "MOP853", text: "일반배관용스테인리스강관" },
|
||||
{ val: "MOP854", text: "전압 콘크리트관" },
|
||||
{ val: "MOP855", text: "주름관" },
|
||||
{ val: "MOP856", text: "주름주철파형강관" },
|
||||
{ val: "MOP857", text: "지중 저압케이블 입상관" },
|
||||
{ val: "MOP858", text: "진동및전압철근콘크리트관" },
|
||||
{ val: "MOP859", text: "철근콘크리트" },
|
||||
{ val: "MOP860", text: "철근콘크리트 블록" },
|
||||
{ val: "MOP861", text: "철근콘크리트조립흙막이" },
|
||||
{ val: "MOP862", text: "철판매입관" },
|
||||
{ val: "MOP863", text: "청동" },
|
||||
{ val: "MOP864", text: "코아식프리스트레스트콘크리트관" },
|
||||
{ val: "MOP865", text: "콘크리트" },
|
||||
{ val: "MOP866", text: "콘크리트 폴륨관" },
|
||||
{ val: "MOP867", text: "콘크리트블록" },
|
||||
{ val: "MOP868", text: "탄소강" },
|
||||
{ val: "MOP869", text: "탄소강(카본스틸)" },
|
||||
{ val: "MOP870", text: "탄소주강" },
|
||||
{ val: "MOP871", text: "트러후" },
|
||||
{ val: "MOP872", text: "파선형강관(RCF)" },
|
||||
{ val: "MOP873", text: "파형강관(CSP)" },
|
||||
{ val: "MOP874", text: "파형관" },
|
||||
{ val: "MOP875", text: "파형플라스틱관" },
|
||||
{ val: "MOP876", text: "폴리머관" },
|
||||
{ val: "MOP877", text: "폴리에스테르수지콘크리트관" },
|
||||
{ val: "MOP878", text: "폴리에틸렌 전선관" },
|
||||
{ val: "MOP879", text: "폴리에틸렌 피복강관" },
|
||||
{ val: "MOP880", text: "폴리에틸렌관 1호관" },
|
||||
{ val: "MOP881", text: "폴리에틸렌관 2호관" },
|
||||
{ val: "MOP882", text: "폴리에틸렌관(2중벽)" },
|
||||
{ val: "MOP883", text: "폴리에틸렌관(3중벽)" },
|
||||
{ val: "MOP884", text: "폴리에틸렌분체라이닝" },
|
||||
{ val: "MOP885", text: "폴리에틸렌이중벽하수관" },
|
||||
{ val: "MOP886", text: "폴리탄산에스테르" },
|
||||
{ val: "MOP887", text: "프리스트레스트콘크리트실린더관" },
|
||||
{ val: "MOP888", text: "플라스틱" },
|
||||
{ val: "MOP889", text: "플라스틱주름관" },
|
||||
{ val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" },
|
||||
{ val: "MOP891", text: "현장타설관" },
|
||||
{ val: "MOP892", text: "현장타설철근콘크리트관" },
|
||||
{ val: "MOP893", text: "흑관" },
|
||||
{ val: "MOP894", text: "흙" },
|
||||
{ val: "MOP895", text: "AP" },
|
||||
{ val: "MOP896", text: "API관" },
|
||||
{ val: "MOP897", text: "ASP" },
|
||||
{ val: "MOP898", text: "CMCIP" },
|
||||
{ val: "MOP899", text: "CSP" },
|
||||
{ val: "MOP900", text: "DRP" },
|
||||
{ val: "MOP901", text: "DSP" },
|
||||
{ val: "MOP902", text: "DTC관" },
|
||||
{ val: "MOP903", text: "ELP" },
|
||||
{ val: "MOP904", text: "EPP" },
|
||||
{ val: "MOP905", text: "FC관" },
|
||||
{ val: "MOP906", text: "H" },
|
||||
{ val: "MOP907", text: "H6" },
|
||||
{ val: "MOP908", text: "HCP" },
|
||||
{ val: "MOP909", text: "HI_2P관" },
|
||||
{ val: "MOP910", text: "HI-KP" },
|
||||
{ val: "MOP911", text: "HL" },
|
||||
{ val: "MOP912", text: "IP" },
|
||||
{ val: "MOP913", text: "KCGP" },
|
||||
{ val: "MOP914", text: "KP관" },
|
||||
{ val: "MOP915", text: "P2S관" },
|
||||
{ val: "MOP916", text: "PC박스(PCB)" },
|
||||
{ val: "MOP917", text: "PCS" },
|
||||
{ val: "MOP918", text: "PDP" },
|
||||
{ val: "MOP919", text: "PE이중벽관" },
|
||||
{ val: "MOP920", text: "PE적측벽1종관" },
|
||||
{ val: "MOP921", text: "PFP관" },
|
||||
{ val: "MOP922", text: "PG관" },
|
||||
{ val: "MOP923", text: "PIP관(이중보온관)" },
|
||||
{ val: "MOP924", text: "PLCSP" },
|
||||
{ val: "MOP925", text: "PS" },
|
||||
{ val: "MOP926", text: "PUB" },
|
||||
{ val: "MOP927", text: "PVB" },
|
||||
{ val: "MOP928", text: "PVC이중벽관(PVC2)" },
|
||||
{ val: "MOP929", text: "PVC-DC" },
|
||||
{ val: "MOP930", text: "PVC-DV" },
|
||||
{ val: "MOP931", text: "SBK" },
|
||||
{ val: "MOP932", text: "SCP" },
|
||||
{ val: "MOP933", text: "SEB" },
|
||||
{ val: "MOP934", text: "SIS관(이중보온관)" },
|
||||
{ val: "MOP935", text: "SMC" },
|
||||
{ val: "MOP936", text: "STW" },
|
||||
{ val: "MOP937", text: "SUS" },
|
||||
{ val: "MOP938", text: "THP나선관" },
|
||||
{ val: "MOP939", text: "U관" },
|
||||
{ val: "MOP940", text: "VG2" },
|
||||
{ val: "MOP941", text: "WSP(와사피)" },
|
||||
{ val: "MOP942", text: "Steel Grating" },
|
||||
{ val: "MOP943", text: "모래" },
|
||||
{ val: "MOP944", text: "인조석" },
|
||||
{ val: "MOP945", text: "SPOL" },
|
||||
{ val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" },
|
||||
{ val: "MOP947", text: "PVC3(PVC3중벽관)" },
|
||||
{ val: "MOP948", text: "PE다중벽관" },
|
||||
{ val: "MOP977", text: "전주용입상관" },
|
||||
{ val: "MOP978", text: "MD관" },
|
||||
];
|
||||
// 하수관로 'fcltyShpCd' '시설물형태코드'
|
||||
export const MS_SWAGE_PPL_SHAPE = [
|
||||
{ val: "FOR000 ", text: "미분류" },
|
||||
{ val: "FOR001 ", text: "원형" },
|
||||
{ val: "FOR002 ", text: "원추형" },
|
||||
{ val: "FOR003 ", text: "원통형" },
|
||||
{ val: "FOR004 ", text: "구형" },
|
||||
{ val: "FOR005 ", text: "타원형" },
|
||||
{ val: "FOR006 ", text: "난원" },
|
||||
{ val: "FOR007 ", text: "반원형" },
|
||||
|
||||
{ val: "FOR010 ", text: "피라미드형" },
|
||||
{ val: "FOR011 ", text: "사면체형" },
|
||||
{ val: "FOR012 ", text: "기둥,축" },
|
||||
{ val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" },
|
||||
{ val: "FOR014 ", text: "아치형" },
|
||||
{ val: "FOR015 ", text: "다중아치형" },
|
||||
{ val: "FOR016 ", text: "돔형" },
|
||||
{ val: "FOR017 ", text: "고형체형" },
|
||||
|
||||
{ val: "FOR020 ", text: "사각형" },
|
||||
{ val: "FOR021 ", text: "평방형" },
|
||||
{ val: "FOR022 ", text: "다이아몬드형" },
|
||||
{ val: "FOR023 ", text: "사다리꼴" },
|
||||
{ val: "FOR024 ", text: "각형" },
|
||||
{ val: "FOR025 ", text: "마제형관" },
|
||||
{ val: "FOR026 ", text: "태형" },
|
||||
|
||||
{ val: "FOR030 ", text: "A형" },
|
||||
{ val: "FOR031 ", text: "H형" },
|
||||
{ val: "FOR032 ", text: "I형" },
|
||||
{ val: "FOR033 ", text: "Y형" },
|
||||
{ val: "FOR034 ", text: "T형" },
|
||||
|
||||
{ val: "FOR050 ", text: "불규칙한 형태" },
|
||||
|
||||
{ val: "FOR900 ", text: "계란형" },
|
||||
{ val: "FOR901 ", text: "구(사각)형" },
|
||||
{ val: "FOR902 ", text: "높이조절철개형" },
|
||||
{ val: "FOR903 ", text: "박스형(RCB)" },
|
||||
{ val: "FOR904 ", text: "반월개거" },
|
||||
{ val: "FOR905 ", text: "반월개거(측구)" },
|
||||
{ val: "FOR906 ", text: "벽돌조" },
|
||||
{ val: "FOR907 ", text: "사각철판" },
|
||||
{ val: "FOR908 ", text: "석축개거" },
|
||||
{ val: "FOR909 ", text: "석축개거(측구)" },
|
||||
{ val: "FOR910 ", text: "승수로" },
|
||||
{ val: "FOR911 ", text: "옹벽개거" },
|
||||
{ val: "FOR912 ", text: "옹벽개거(측구)" },
|
||||
{ val: "FOR913 ", text: "이단" },
|
||||
{ val: "FOR914 ", text: "이중침전형" },
|
||||
{ val: "FOR915 ", text: "자연개거" },
|
||||
{ val: "FOR916 ", text: "자연개거(측구)" },
|
||||
{ val: "FOR917 ", text: "조립식" },
|
||||
{ val: "FOR918 ", text: "지상관" },
|
||||
{ val: "FOR919 ", text: "지상단구" },
|
||||
{ val: "FOR920 ", text: "지상쌍구" },
|
||||
{ val: "FOR921 ", text: "지하단구" },
|
||||
{ val: "FOR922 ", text: "지하쌍구" },
|
||||
{ val: "FOR923 ", text: "직사각형" },
|
||||
{ val: "FOR924 ", text: "타설식" },
|
||||
{ val: "FOR925 ", text: "토류형" },
|
||||
{ val: "FOR926 ", text: "흄관원형" },
|
||||
{ val: "FOR927 ", text: "L형측구" },
|
||||
{ val: "FOR928 ", text: "LO형측구" },
|
||||
{ val: "FOR929 ", text: "LU형측구" },
|
||||
{ val: "FOR930 ", text: "PVC이중벽관" },
|
||||
{ val: "FOR931 ", text: "RCB 맨홀" },
|
||||
{ val: "FOR932 ", text: "U형" },
|
||||
{ val: "FOR933 ", text: "U형측구(미복개)" },
|
||||
{ val: "FOR934 ", text: "U형측구(복개)" },
|
||||
|
||||
{ val: "FOR999 ", text: "기타" }
|
||||
];
|
||||
// 하수처리시설
|
||||
export const SwtrmtFcltCol = [
|
||||
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
|
||||
{ id: 'fclt_cpct', title: '시설용량(㎥/d)' },
|
||||
{ id: 'rgn_expln', title: '지역설명' },
|
||||
{ id: 'cmcn_ymd', title: '준공일자' },
|
||||
{ id: 'oprtng_strt_ymd', title: '가동개시일자' },
|
||||
{ id: 'clsg_ymd', title: '폐쇄일자' },
|
||||
// { id: 'prcs_msrc_cd', title: '처리공법코드' },
|
||||
// { id: 'lae_cd', title: '지자체코드' },
|
||||
{ id: 'lae_nm', title: '지자체명' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
// { id: 'sarea_cd', title: '권역코드' },
|
||||
{ id: 'sarea_nm', title: '권역명' },
|
||||
{ id: 'lrge_sarea_nm', title: '대권역명' },
|
||||
{ id: 'rvr_nm', title: '하천명' },
|
||||
{ id: 'addr', title: '주소' },
|
||||
{ id: 'dtl_addr', title: '상세주소' },
|
||||
{ id: 'rdnm_addr', title: '도로명주소' },
|
||||
{ id: 'dtl_rdnm_addr', title: '상세도로명주소' },
|
||||
{ id: 'dschg_wtst_nm', title: '방류수계명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
{ id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' },
|
||||
{ id: 'swtrmt_ppltn_cnt', title: '하수처리인구수(인)' },
|
||||
{ id: 'prcs_zone_area_sum', title: '처리구역면적합계(m²)' },
|
||||
{ id: 'xcrd', title: 'X좌표' },
|
||||
{ id: 'ycrd', title: 'Y좌표' },
|
||||
// { id: 'oprtngSttsCd', title: '가동상태코드' },
|
||||
{ id: 'rprs_msrc_cn', title: '대표공법내용' },
|
||||
// { id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량(mg/L)' },
|
||||
// { id: 'stty_crtr_toc', title: '법정기준TOC((mg/L))' },
|
||||
// { id: 'stty_crtr_ss', title: '법정기준SS(mg/L)' },
|
||||
// { id: 'stty_crtr_tn', title: '법정기준총질소(mg/L)' },
|
||||
// { id: 'stty_crtr_tph', title: '법정기준총인(mg/L)' },
|
||||
// { id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수(군수/100mL)' },
|
||||
{ id: 'bsn_nm', title: '유역명' },
|
||||
// { id: 'obsrvnSptCd', title: '관측지점코드' },
|
||||
];
|
||||
export const SwtrmtFcltCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
{ id: 'swtrmtFcltNm', title: '하수처리\n시설명' },
|
||||
{ id: 'fcltCpct', title: '시설\n용량\n(㎥/d)' },
|
||||
{ id: 'rgnExpln', title: '지역설명' },
|
||||
// { id: 'cmcnYmd', title: '준공일자' },
|
||||
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
|
||||
// { id: 'clsgYmd', title: '폐쇄일자' },
|
||||
// { id: 'prcsMsrcCd', title: '처리공법코드' },
|
||||
// { id: 'laeCd', title: '지자체코드' },
|
||||
{ id: 'laeNm', title: '지자체명' },
|
||||
// { id: 'ctpvNm', title: '시도명' },
|
||||
// { id: 'sggNm', title: '시군구명' },
|
||||
// { id: 'sareaCd', title: '권역코드' },
|
||||
{ id: 'sareaNm', title: '권역명' },
|
||||
// { id: 'lrgeSareaNm', title: '대권역명' },
|
||||
{ id: 'rvrNm', title: '하천명' },
|
||||
// { id: 'addr', title: '주소' },
|
||||
// { id: 'dtlAddr', title: '상세주소' },
|
||||
// { id: 'rdnmAddr', title: '도로명주소' },
|
||||
// { id: 'dtlRdnmAddr', title: '상세도로명주소' },
|
||||
{ id: 'dschgWtstNm', title: '방류수계명' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'mngRprtvBzentyNm', title: '관리대행업체명' },
|
||||
{ id: 'swtrmtPpltnCnt', title: '하수\n처리\n인구수\n(인)' },
|
||||
// { id: 'prcsZoneAreaSum', title: '처리구역면적합계(m²)' },
|
||||
// { id: 'xcrd', title: 'X좌표' },
|
||||
// { id: 'ycrd', title: 'Y좌표' },
|
||||
// { id: 'oprtngSttsCd', title: '가동상태코드' },
|
||||
{ id: 'rprsMsrcCn', title: '대표\n공법\n내용' },
|
||||
// { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량(mg/L)' },
|
||||
// { id: 'sttyCrtrToc', title: '법정기준TOC(mg/L)' },
|
||||
// { id: 'sttyCrtrSs', title: '법정기준SS(mg/L)' },
|
||||
// { id: 'sttyCrtrTn', title: '법정기준총질소(mg/L)' },
|
||||
// { id: 'sttyCrtrTph', title: '법정기준총인(mg/L)' },
|
||||
// { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수(군수/100mL)' },
|
||||
// { id: 'bsnNm', title: '유역명' },
|
||||
// { id: 'obsrvnSptCd', title: '관측지점코드' },
|
||||
];
|
||||
export const SwtrmtFcltExcelCol = {
|
||||
'swtrmtFcltNm': '하수처리시설명',
|
||||
'fcltCpct': '시설 용량(㎥/d)',
|
||||
'rgnExpln': '지역설명',
|
||||
'oprtngStrtYmd': '가동개시 일자',
|
||||
'laeNm': '지자체명',
|
||||
'sareaNm': '권역명',
|
||||
'rvrNm': '하천명',
|
||||
'dschgWtstNm': '방류수계명',
|
||||
'swtrmtPpltnCnt': '하수 처리 인구수(인)',
|
||||
'rprsMsrcCn': '대표 공법 내용'
|
||||
};
|
||||
|
||||
// 하수처리구역
|
||||
export const SwtrmtPrcsZoneCol = [
|
||||
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
|
||||
{ id: 'swtrmt_zone_nm', title: '하수처리구역명' },
|
||||
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
|
||||
// { id: 'ctpv_cd', title: '시도코드' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
// { id: 'sgg_cd', title: '시군구코드' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
|
||||
];
|
||||
export const SwtrmtPrcsZoneCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
|
||||
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
|
||||
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
|
||||
// { id: 'ctpv_cd', title: '시도코드' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
// { id: 'sgg_cd', title: '시군구코드' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
|
||||
];
|
||||
export const SwtrmtPrcsZoneExcelCol = {
|
||||
'swtrmtZoneNm': '하수처리구역명',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swtrmtFcltNm': '하수처리시설명'
|
||||
};
|
||||
|
||||
// 침수흔적도
|
||||
export const FldngWtlvMapCol = [
|
||||
{ id: 'fldng_yr', title: '침수년도' },
|
||||
{ id: 'fldng_dstr_nm', title: '침수재해명' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
{ id: 'fldng_bgng_dtm', title: '침수시작일시' },
|
||||
{ id: 'fldng_end_dtm', title: '침수종료일시' },
|
||||
{ id: 'fldng_hr', title: '침수시간' },
|
||||
{ id: 'fldng_dstrct_nm', title: '침수지구명' },
|
||||
{ id: 'fldng_cs_dtl_cd', title: '침수원인상세코드' },
|
||||
{ id: 'dtl_ocrn_cs_cn', title: '침수발생원인' },
|
||||
{ id: 'avg_fldng_wtlv', title: '평균침수위' },
|
||||
{ id: 'fldng_area', title: '침수면적' },
|
||||
{ id: 'exmn_yr', title: '조사년도' },
|
||||
{ id: 'dam_amt', title: '피해금액' },
|
||||
{ id: 'rain_bgng_tm', title: '강우시작일시' },
|
||||
{ id: 'rain_end_tm', title: '강우종료일시' },
|
||||
{ id: 'rain_tot', title: '총강우량(mm)' },
|
||||
{ id: 'rain_str', title: '강우강도(mm/hr)' },
|
||||
];
|
||||
export const FldngWtlvMapCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
{ id: 'fldngYr', title: '침수년도' },
|
||||
{ id: 'fldngDstrNm', title: '침수재해명' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: 'fldngBgngDtm', title: '침수시작일시' },
|
||||
{ id: 'fldngEndDtm', title: '침수종료일시' },
|
||||
{ id: 'fldngHr', title: '침수시간' },
|
||||
{ id: 'fldngDstrctNm', title: '침수지구명' },
|
||||
{ id: 'fldngCsDtlCd', title: '침수원인상세코드' },
|
||||
{ id: 'avgFldngWtlv', title: '평균침수위' },
|
||||
{ id: 'fldngArea', title: '침수면적' },
|
||||
{ id: 'exmnYr', title: '조사년도' },
|
||||
{ id: 'damAmt', title: '피해금액' },
|
||||
{ id: 'rainBgngTm', title: '강우시작일시' },
|
||||
{ id: 'rainEndTm', title: '강우종료일시' },
|
||||
{ id: 'rainTot', title: '총강우량(mm)' },
|
||||
{ id: 'rainStr', title: '강우강도(mm/hr)' },
|
||||
];
|
||||
export const FldngDpthMapCol = [
|
||||
{ id: 'fldng_yr', title: '침수년도' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
{ id: 'fldng_dpth', title: '침수깊이(m)' },
|
||||
{ id: 'fldng_area', title: '침수면적(㎡)' },
|
||||
// { id: 'shape_area', title: '형상면적(㎡)' },
|
||||
// { id: 'shape_len', title: '형상길이' },
|
||||
];
|
||||
/** 기본정보 목록 API의 camelCase 속성을 침수흔적심선 그리드에 연결한다. */
|
||||
export const FldngDpthMapCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
{ id: 'fldngYr', title: '침수년도' },
|
||||
{ id: 'ctpvNm', title: '시도명' },
|
||||
{ id: 'sggNm', title: '시군구명' },
|
||||
{ id: 'fldngDpth', title: '침수깊이(m)' },
|
||||
{ id: 'fldngArea', 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' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'last_drdgn_ymd', title: '최종준설일자' },
|
||||
// { id: 'usg_cd', title: '용도코드' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
// { id: 'mhl_knd_cd', title: '맨홀종류코드' },
|
||||
{ id: 'swage_mhL_diam_vl', title: '하수맨홀구경(mm)' },
|
||||
{ id: 'mqlt_cd', title: '재질' },
|
||||
{ id: 'wdth_len', title: '가로길이(m)' },
|
||||
{ id: 'vrtc_len', title: '세로길이(m)' },
|
||||
// { id: 'ang_dir', title: '표고' },
|
||||
{ id: 'altd_vl', title: '지표고(EL.m)' },
|
||||
{ id: 'dpthl_vl', title: '맨홀저고(EL.m)' },
|
||||
// { id: 'inst_mng_no', title: '기관관리번호' }
|
||||
];
|
||||
export const MsSwageMhlCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'lastDrdgnYmd', title: '최종준설일자' },
|
||||
{ id: 'usgCd', title: '용도' },
|
||||
// { id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
// { id: 'mhlKndCd', title: '맨홀종류코드' },
|
||||
{ id: 'swageMhlDiamVl', title: '하수맨홀구경(mm)' },
|
||||
// { id: 'mqltCd', title: '재질코드' },
|
||||
{ id: 'wdthLen', title: '가로길이(m)' },
|
||||
{ id: 'vrtcLen', title: '세로길이(m)' },
|
||||
// { id: 'angDir', title: '표고' },
|
||||
{ id: 'altdVl', title: '지표고(EL.m)' },
|
||||
{ id: 'dpthlVl', title: '맨홀저고(EL.m)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' }
|
||||
];
|
||||
export const MsSwageMhlExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'instlYmd': '설치일자',
|
||||
'usgCd': '용도',
|
||||
'swageMhlDiamVl': '하수맨홀구경(mm)',
|
||||
'wdthLen': '가로길이(m)',
|
||||
'vrtcLen': '세로길이(m)',
|
||||
'altdVl': '지표고(EL.m)',
|
||||
'dpthlVl': '맨홀저고(EL.m)'
|
||||
};
|
||||
// 하수맨홀 뚜껑재질 코드표
|
||||
export const MS_SWAGE_MHL_CODES = [
|
||||
{ val: "SBC000", text: "미분류" },
|
||||
{ val: "SBC001", text: "주철" },
|
||||
{ val: "SBC002", text: "철판" },
|
||||
{ val: "SBC003", text: "콘크리트" },
|
||||
{ val: "SBC004", text: "그레이팅" },
|
||||
{ val: "SBC091", text: "칼라" },
|
||||
{ val: "SBC092", text: "S.G" },
|
||||
{ val: "SBC999", text: "기타" },
|
||||
{ val: "SBC900", text: "각주철" },
|
||||
{ val: "SBC901", text: "강재" },
|
||||
{ val: "SBC902", text: "나무" },
|
||||
{ val: "SBC903", text: "나무,널판지,합판" },
|
||||
{ val: "SBC904", text: "대리석" },
|
||||
{ val: "SBC905", text: "뚜껑없음" },
|
||||
{ val: "SBC906", text: "보도용콘크리트" },
|
||||
{ val: "SBC907", text: "스틸(ST)" },
|
||||
{ val: "SBC908", text: "신형 CON" },
|
||||
{ val: "SBC909", text: "알루미늄" },
|
||||
{ val: "SBC910", text: "염화비닐(PE)" },
|
||||
{ val: "SBC911", text: "인조석" },
|
||||
{ val: "SBC912", text: "인조석(주철)" },
|
||||
{ val: "SBC913", text: "인조CONC" },
|
||||
{ val: "SBC914", text: "철, 철근" },
|
||||
{ val: "SBC915", text: "칼라콘크리트" },
|
||||
{ val: "SBC916", text: "플라스틱" },
|
||||
{ val: "SBC917", text: "PVC" },
|
||||
{ val: "SBC918", text: "SP" },
|
||||
{ val: "SBC919", text: "SS" },
|
||||
{ val: "SBC920", text: "닥타일주철" },
|
||||
{ val: "SBC921", text: "철근G" },
|
||||
{ val: "SBC922", text: "폴리에틸렌(PE)" },
|
||||
{ val: "SBC923", text: "회주철" }
|
||||
];
|
||||
// 하수맨홀 용도 코드표
|
||||
export const MS_SWAGE_MHL_USE_CODES = [
|
||||
{ val: "SMU000 ", text: "미분류" },
|
||||
{ val: "SMU001 ", text: "우수" },
|
||||
{ val: "SMU002 ", text: "오수" },
|
||||
{ val: "SMU003 ", text: "집수" },
|
||||
{ val: "SMU004 ", text: "합류" },
|
||||
{ val: "SMU005 ", text: "차집" },
|
||||
{ val: "SMU999 ", text: "기타" },
|
||||
{ val: "SMU900 ", text: "1호" },
|
||||
{ val: "SMU901 ", text: "2호" },
|
||||
{ val: "SMU902 ", text: "3호" },
|
||||
{ val: "SMU903 ", text: "4호" },
|
||||
{ val: "SMU904 ", text: "5호" },
|
||||
{ val: "SMU905 ", text: "6호" },
|
||||
{ val: "SMU906 ", text: "7호" },
|
||||
{ val: "SMU907 ", text: "8호" },
|
||||
{ val: "SMU908 ", text: "구거용맨홀" },
|
||||
{ val: "SMU909 ", text: "부관부착맨홀" },
|
||||
{ val: "SMU910 ", text: "조립식맨홀" },
|
||||
{ val: "SMU911 ", text: "특1호" },
|
||||
{ val: "SMU912 ", text: "특2호" },
|
||||
{ val: "SMU913 ", text: "특3호" },
|
||||
{ val: "SMU914 ", text: "특4호" },
|
||||
{ val: "SMU915 ", text: "특5호" },
|
||||
{ val: "SMU916 ", text: "하수맨홀용도" },
|
||||
{ val: "SMU917 ", text: "현장타설관거용" }
|
||||
];
|
||||
|
||||
// 우수펌프장
|
||||
export const ExclnSewagePumpCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altd_vl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlv_vl', title: '수위(m)' },
|
||||
{ id: 'excln_wpmp_ablt_vl', title: '우수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const ExclnSewagePumpCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'swagePmstnNm', title: '하수펌프장명' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altdVl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlvVl', title: '수위(m)' },
|
||||
{ id: 'exclnWpmpAbltVl', title: '우수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const ExclnSewagePumpExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swagePmstnNm': '하수펌프장명',
|
||||
'instlYmd': '설치일자',
|
||||
'dailyPrcsCpct': '일일처리용량(㎥)',
|
||||
'maxImpndCpct': '최대저수용량(㎥)',
|
||||
'altdVl': '표고(EL.m)',
|
||||
'wtlvVl': '수위(m)',
|
||||
'exclnWpmpAbltVl': '우수양수능력(m²/d)'
|
||||
};
|
||||
// 오수펌프장
|
||||
export const DrtwSewagePumpCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altd_vl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlv_vl', title: '수위(m)' },
|
||||
{ id: 'excln_wpmp_ablt_vl', title: '오수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const DrtwSewagePumpCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'swagePmstnNm', title: '하수펌프장명' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altdVl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlvVl', title: '수위(m)' },
|
||||
{ id: 'exclnWpmpAbltVl', title: '오수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
// 펌프장 용도 코드표
|
||||
export const DRTW_SEWAGE_PUMP_USE_CODES = [
|
||||
{ val: "SBE000", text: "미분류" },
|
||||
{ val: "SBE001", text: "빗물펌프장" },
|
||||
{ val: "SBE002", text: "오수펌프장" },
|
||||
{ val: "SBE003", text: "처리장내펌프장" },
|
||||
{ val: "SBE900", text: "오수배제" },
|
||||
{ val: "SBE901", text: "우수배제" },
|
||||
{ val: "SBE999", text: "기타" },
|
||||
];
|
||||
|
||||
// 우수토실
|
||||
export const stofcbrCol = [
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
{ id: 'worflw_wall_hgt', title: '월류벽높이(m)' },
|
||||
// { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' },
|
||||
// { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' },
|
||||
{ id: 'swage_mhl_mng_no', title: '하수맨홀관리번호' },
|
||||
{ id: 'drtn_angl', title: '방향각도(°)' },
|
||||
]
|
||||
export const stofcbrCol2 = [
|
||||
{ id: 'rowNumber', title: '순번' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
{ id: 'worflwWallHgt', title: '월류벽높이(m)' },
|
||||
// { id: 'mhlExstYnCd', title: '맨홀존재여부코드' },
|
||||
// { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' },
|
||||
{ id: 'swageMhlMngNo', title: '하수맨홀관리번호' },
|
||||
{ id: 'drtnAngl', title: '방향각도(°)' },
|
||||
]
|
||||
export const stofcbrExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'mapshtNo': '도엽번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'instlYmd': '설치일자',
|
||||
'worflwWallHgt': '월류벽높이(m)',
|
||||
'swageMhlMngNo': '하수맨홀관리번호',
|
||||
'drtnAngl': '방향각도(°)'
|
||||
};
|
||||
|
||||
// 중점우수토실 측정지점
|
||||
export const untrtSwgRainCol1 = [
|
||||
{ id: 'manage_no', title: '관리번호' },
|
||||
{ id: 'swr_ppl_eqp_nm', title: '처리구역' },
|
||||
{ id: 'swr_ppl_trt_nm', title: '처리분구' },
|
||||
{ id: 'addr', title: '인근주소' },
|
||||
{ id: 'chmbr_xcnts', title: '토실 위도' },
|
||||
{ id: 'chmbr_ydnts', title: '토실 경도' }
|
||||
];
|
||||
|
||||
// 중점우수토실 측정지점 시설현황
|
||||
export const untrtSwgRainCol2 = [
|
||||
{ id: 'mesure_spot_se', title: '구분' }, // 유량계측정방식 기타
|
||||
{ id: 'discharge_pipe_spec', title: '방류관로 규격(m)' },
|
||||
{ id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' },
|
||||
{ id: 'swr_trt_popl', title: '하수처리 인구(인)' },
|
||||
{ id: 'screen_instl_at', title: '스크린설치 여부' },
|
||||
{ id: 'weir_hg', title: '위어 높이(m)' },
|
||||
{ id: 'weir_lt', title: '위어 길이(m)' }
|
||||
];
|
||||
|
||||
// 중점우수토실 강우정보 (id값 틀릴 수 있음)
|
||||
export const untrtSwgRainCol3 = [
|
||||
{ id: 'rainBeginDt', title: '강우시작일시' },
|
||||
{ id: 'rainEndDt', title: '강우종료일시' },
|
||||
{ id: 'precdngDry', title: '선행건기(h)' },
|
||||
{ id: 'rainInfo', title: '강우정보' },
|
||||
{ id: 'rainFall', title: '누적강우량(mm)' },
|
||||
{ id: 'cntncTime', title: '지속시간(h)' },
|
||||
{ id: 'rainIn', title: '강우강도(mm/h)' }
|
||||
];
|
||||
|
||||
// 중점우수토실 측정정보 (id값 틀릴 수 있음)
|
||||
export const untrtSwgRainCol4 = [
|
||||
{ id: 'mesureBeginDt', title: '측정시작일시' },
|
||||
{ id: 'mesureEndDt', title: '측정종료일시' },
|
||||
{ id: 'mesureIntrvl', title: '측정간격(분)' },
|
||||
{ id: 'mesureMhrlsInstlLc', title: '측정기기 설치위치' },
|
||||
{ id: 'flwmtrCombined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼
|
||||
{ id: 'qltwtrBeginDt', title: '채수시작일시' },
|
||||
{ id: 'qltwtrEndDt', title: '채수종료일시' },
|
||||
{ id: 'qltwtrCo', title: '채수횟수' }
|
||||
];
|
||||
|
||||
// 중점우수토실 수량/수질측정 정보
|
||||
export const untrtSwgRainCol5 = [
|
||||
{ id: 'odr', title: '회차' },
|
||||
{ id: 'mesureDt', title: '측정일자' },
|
||||
{ id: 'mesureQy', title: '수량(m³)' },
|
||||
{ id: 'mesureBod', title: 'BOD(mg/L)' },
|
||||
{ id: 'mesureToc', title: 'TOC(mg/L)' },
|
||||
{ id: 'mesureLoad', title: '오염부하량(kg)' }
|
||||
];
|
||||
|
||||
// 🎯 날짜 필드들 (yyyymmdd -> yyyy년 mm월 dd일)
|
||||
export const dateFields = [
|
||||
'instl_ymd', 'last_drdgn_ymd', 'cmcn_ymd', 'oprtng_strt_ymd', 'clsg_ymd',
|
||||
'mesure_begin_dt', 'mesure_end_dt', 'qltwtr_begin_dt', 'qltwtr_end_dt',
|
||||
'rain_begin_dt', 'rain_end_dt', 'mesureDt',
|
||||
// 🎯 camelCase 버전 추가
|
||||
'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd',
|
||||
'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt',
|
||||
'rainBeginDt', 'rainEndDt',
|
||||
'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr',
|
||||
'bgng_ymd', 'end_ymd', 'bgngYmd', 'endYmd'
|
||||
];
|
||||
|
||||
// 🎯 연월 필드들 (yyyymm -> yyyy년 m월)
|
||||
export const yearMonthFields = ['aplcnYm',];
|
||||
|
||||
// 🎯 날짜시간 필드들 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm)
|
||||
export const dateTimeFields = ['regDt',];
|
||||
|
||||
// 🎯 숫자 필드들 (천 단위 구분자)
|
||||
export const numberFields = [
|
||||
'pdmt', 'prlg', 'avg_grdn', 'drtw_mltv_area', 'excln_mltv_area', 'tmpt_dpth', 'scend_dpth', 'trfln_cnt', 'vrtc_len', 'wdth_len',
|
||||
'fclt_cpct', 'plln_area', 'prcs_zone_area_sum', 'swtrmt_ppltn_cnt', 'stty_crtr_bod', 'stty_crtr_toc', 'stty_crtr_ss', 'stty_crtr_tn', 'stty_crtr_tph', 'stty_crtr_clbcl_cnt',
|
||||
'swage_mh_diam', 'wdth_len', 'vrtc_len', 'altd_vl', 'dpthlVl',
|
||||
'plln_area', 'daily_prcs_cpct', 'max_impnd_cpct', 'altd_vl', 'wtlv_vl', 'excln_wpmp_ablt',
|
||||
'worflw_wall_hgt',
|
||||
'swr_ppl_eqp_ar', 'swr_trt_popl', 'weir_hg', 'weir_lt',
|
||||
'precdng_dry', 'rain_fall', 'cntnc_time', 'rain_in',
|
||||
'mesure_intrvl', 'qltwtr_co', 'mesureQy', 'mesureBod', 'mesureToc', 'mesureLoad',
|
||||
// 🎯 camelCase 버전 추가
|
||||
'avgGrdn', 'drtwMltvArea', 'exclnMltvArea', 'tmptDpth', 'scendDpth', 'trflnCnt', 'vrtcLen', 'wdthLen',
|
||||
'fcltCpct', 'pllnArea', 'prcsZoneAreaSum', 'swtrmtPpltnCnt', 'sttyCltrBod', 'sttyCltrToc', 'sttyCltrSs', 'sttyCltrTn', 'sttyCltrTph', 'sttyCltrClbclCnt',
|
||||
'swageMhDiam', 'altdVl', 'dpthlVl',
|
||||
'dailyPrcsCpct', 'maxImpndCpct', 'wtlvVl', 'exclnWpmpAblt',
|
||||
'worflwWallHgt',
|
||||
'swrPplEqpAr', 'swrTrtPopl', 'weirHg', 'weirLt',
|
||||
'precdngDry', 'rainFall', 'cntncTime', 'rainIn',
|
||||
'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc',
|
||||
'bizAmt', 'count',
|
||||
'chmbr_xcnts', 'chmbr_ydnts',
|
||||
'fldng_hr', 'avg_fldng_wtlv', 'fldng_area', 'dam_amt',
|
||||
'fldngHr', 'avgFldngWtlv', 'fldngArea', 'damAmt'
|
||||
];
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
type Props = {
|
||||
jobType: ExmnMapJobType;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/** 관로조사 지도 데이터 조회 API와 응답 변환 함수를 제공한다. */
|
||||
import { getRawJson } from "../../../../components/lib/api";
|
||||
import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
|
||||
|
||||
type ExmnLayerScope = {
|
||||
sggCd?: string;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
/** 침수흔적 강우분석과 연결된 심선·위선 목록 조회를 담당한다. */
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
|
||||
import type { FldngHstryDetailCacheValue } from "./fldngHstryTypes";
|
||||
import type { FldngHstryDetailCacheValue } from "@/entities/flood-history/model/flood-history-types";
|
||||
|
||||
const normalizeFldngHstryRainAnalysisPayload = (data: any) => {
|
||||
if (!data || typeof data !== "object") return {};
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { FldngHstryItem } from "./fldngHstryTypes";
|
||||
import { formatFldngHstryArea, formatFldngHstryDepth } from "./fldngHstryUtils";
|
||||
import type { FldngHstryItem } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { formatFldngHstryArea, formatFldngHstryDepth } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
|
||||
type Props = {
|
||||
detail: FldngHstryItem;
|
||||
+2
-6
@@ -10,12 +10,8 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import type Feature from "ol/Feature.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
|
||||
import { loadFldngHstryDetail } from "./fldngHstryClient";
|
||||
import {
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
type FldngHstryDetailCacheValue,
|
||||
} from "./fldngHstryTypes";
|
||||
import { loadFldngHstryDetail } from "./fldng-hstry-client";
|
||||
import { FLDNG_HSTRY_DEPTH_LAYER_TYPE, FLDNG_HSTRY_LAYER_TYPE, type FldngHstryDetailCacheValue } from "@/entities/flood-history/model/flood-history-types";
|
||||
|
||||
type FldngHstryGisLayer = {
|
||||
searchLayer?: { getSource?: () => VectorSource | null } | null;
|
||||
+3
-6
@@ -5,12 +5,9 @@
|
||||
"use client";
|
||||
|
||||
import { SelectBox } from "@common/business_components/ui";
|
||||
import {
|
||||
FLDNG_HSTRY_MIN_YEAR,
|
||||
type FldngHstryYearRange,
|
||||
} from "./fldngHstryTypes";
|
||||
import { normalizeFldngHstryYearRange } from "./fldngHstryUtils";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
import { FLDNG_HSTRY_MIN_YEAR, type FldngHstryYearRange } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { normalizeFldngHstryYearRange } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
type Props = {
|
||||
yearRange: FldngHstryYearRange;
|
||||
+8
-23
@@ -7,31 +7,16 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { requestGridExcel } from "@/shared/api/grid-export";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import {
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_DEFAULT_FILTERS,
|
||||
FLDNG_HSTRY_LIST_HEADLIST,
|
||||
FLDNG_HSTRY_PAGE_SIZE,
|
||||
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
type FldngHstryApiItem,
|
||||
type FldngHstryFilters,
|
||||
type FldngHstryTableRow,
|
||||
} from "./fldngHstryTypes";
|
||||
import {
|
||||
attachFldngHstryGeometry,
|
||||
buildFldngHstryListQueryString,
|
||||
buildFldngHstryYearOptions,
|
||||
normalizeFldngHstryFilters,
|
||||
toFldngHstryTableRows,
|
||||
} from "./fldngHstryUtils";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { FLDNG_HSTRY_LAYER_TYPE, FLDNG_HSTRY_DEFAULT_FILTERS, FLDNG_HSTRY_LIST_HEADLIST, FLDNG_HSTRY_PAGE_SIZE, FLDNG_HSTRY_ROW_SELECT_MESSAGE, type FldngHstryApiItem, type FldngHstryFilters, type FldngHstryTableRow } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { attachFldngHstryGeometry, buildFldngHstryListQueryString, buildFldngHstryYearOptions, normalizeFldngHstryFilters, toFldngHstryTableRows } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
|
||||
const unwrapPayload = <T,>(response: any): T =>
|
||||
(response?.data ?? response) as T;
|
||||
+7
-7
@@ -7,12 +7,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import useStore from "@/store/useStore";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import {
|
||||
FLOOD_DNGR_MAP_DEFAULT_FILTERS,
|
||||
FLOOD_DNGR_MAP_DEPTH_OPTIONS,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
type FloodDngrMapExtent,
|
||||
type FloodDngrMapFilters,
|
||||
type FloodDngrMapTableRow,
|
||||
} from "./floodDngrMapTypes";
|
||||
} from "./flood-dngr-map-types";
|
||||
|
||||
const FLOOD_DNGR_MAP_REQUEST_TIMEOUT_MS = 10_000;
|
||||
const FLOOD_DNGR_MAP_LOADING_WATCHDOG_MS = 12_000;
|
||||
+61
-22
@@ -4,15 +4,19 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
|
||||
import { Button } from "@common/components/ui";
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { VerticalTable } from "@/components/ui/vertical-table";
|
||||
|
||||
import type { GisEditChange, GisEditPipeBasicInfo } from "./gisEditTypes";
|
||||
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
|
||||
import type { GisEditChange, GisEditPipeBasicInfo } from "./gis-edit-types";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
} from "@/entities/facility/model/facility-code-fallbacks";
|
||||
|
||||
type Props = {
|
||||
change: GisEditChange;
|
||||
@@ -206,20 +210,25 @@ const normalizeBasicFormValues = (values: Record<string, any>) => {
|
||||
return nextValues;
|
||||
};
|
||||
|
||||
const buildBaseFeatureValues = (featureData: any) => {
|
||||
const buildBaseFeatureValues = (
|
||||
featureData: any,
|
||||
catalog?: ReturnType<typeof useFacilityCodeCatalog>,
|
||||
) => {
|
||||
const resolveUsageCode = (value: unknown) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
const matched = MS_SWAGE_PPL_USE_CODES.find(
|
||||
(item) => item.val === raw || item.text === raw,
|
||||
);
|
||||
const matched = [
|
||||
...(catalog?.MS_SWAGE_PPL_USE_CODES ?? []),
|
||||
...MS_SWAGE_PPL_USE_CODES,
|
||||
].find((item) => item.val === raw || item.text === raw);
|
||||
return matched?.val.trim() ?? raw;
|
||||
};
|
||||
|
||||
const resolveMaterialCode = (value: unknown) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
const matched = MS_SWAGE_PPL_CODES.find(
|
||||
(item) => item.val === raw || item.text === raw,
|
||||
);
|
||||
const matched = [
|
||||
...(catalog?.MS_SWAGE_PPL_CODES ?? []),
|
||||
...MS_SWAGE_PPL_CODES,
|
||||
].find((item) => item.val === raw || item.text === raw);
|
||||
return matched?.val.trim() ?? raw;
|
||||
};
|
||||
|
||||
@@ -243,13 +252,12 @@ const buildBaseFeatureValues = (featureData: any) => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const getPipeUsageLabel = (value: unknown) => {
|
||||
const getPipeUsageLabel = (value: unknown, codes = MS_SWAGE_PPL_USE_CODES) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "";
|
||||
return (
|
||||
MS_SWAGE_PPL_USE_CODES.find(
|
||||
(item) => item.val.trim() === raw || item.text === raw,
|
||||
)?.text ?? raw
|
||||
codes.find((item) => item.val.trim() === raw || item.text === raw)?.text ??
|
||||
raw
|
||||
);
|
||||
};
|
||||
|
||||
@@ -269,10 +277,17 @@ export default function GisEditPipeSessionPopup({
|
||||
onClose,
|
||||
onSave,
|
||||
}: Props) {
|
||||
const catalog = useFacilityCodeCatalog();
|
||||
// CMS 명칭 갱신은 입력 중인 초안을 초기화할 이유가 아니다.
|
||||
const initializedSource = useRef<{
|
||||
featureData: any;
|
||||
changeId: string;
|
||||
isNewPipe: boolean;
|
||||
} | null>(null);
|
||||
const isNewPipe = change.editType === "ADD";
|
||||
const baseFeatureValues = useMemo(
|
||||
() => buildBaseFeatureValues(featureData),
|
||||
[featureData],
|
||||
() => buildBaseFeatureValues(featureData, catalog),
|
||||
[featureData, catalog],
|
||||
);
|
||||
const [basicInfoTableData, setBasicInfoTableData] = useState<
|
||||
Record<string, any>
|
||||
@@ -322,12 +337,36 @@ export default function GisEditPipeSessionPopup({
|
||||
PIPE_BASIC_INFO_FIELDS.map((field) =>
|
||||
field.id === "mngInstCd"
|
||||
? { ...field, options: mngInstSelectOptions }
|
||||
: field,
|
||||
: field.id === "usgCd" || field.id === "mqltCd"
|
||||
? {
|
||||
...field,
|
||||
options: [
|
||||
EMPTY_SELECT_OPTION,
|
||||
...(field.id === "usgCd"
|
||||
? catalog.MS_SWAGE_PPL_USE_CODES
|
||||
: catalog.MS_SWAGE_PPL_CODES
|
||||
).map((item) => ({ value: item.val, label: item.text })),
|
||||
],
|
||||
}
|
||||
: field,
|
||||
),
|
||||
[mngInstSelectOptions],
|
||||
[mngInstSelectOptions, catalog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const previous = initializedSource.current;
|
||||
if (
|
||||
previous &&
|
||||
previous.featureData === featureData &&
|
||||
previous.changeId === change.changeId &&
|
||||
previous.isNewPipe === isNewPipe
|
||||
)
|
||||
return;
|
||||
initializedSource.current = {
|
||||
featureData,
|
||||
changeId: change.changeId,
|
||||
isNewPipe,
|
||||
};
|
||||
const initialBasicValues: Record<string, any> = {
|
||||
...baseFeatureValues,
|
||||
...normalizeBasicFormValues(change.basicInfo ?? {}),
|
||||
@@ -335,7 +374,7 @@ export default function GisEditPipeSessionPopup({
|
||||
|
||||
setBasicInfoTableData(initialBasicValues);
|
||||
setInputInfoTableData(buildInitialInputDraft(change));
|
||||
}, [baseFeatureValues, change.changeId, isNewPipe]);
|
||||
}, [baseFeatureValues, featureData, change.changeId, isNewPipe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewPipe) return;
|
||||
@@ -429,7 +468,7 @@ export default function GisEditPipeSessionPopup({
|
||||
<h3 className="whitespace-nowrap text-2xl font-medium text-primary-foreground">
|
||||
하수관로
|
||||
{basicInfoTableData?.usgCd
|
||||
? `(${getPipeUsageLabel(basicInfoTableData.usgCd)})`
|
||||
? `(${getPipeUsageLabel(basicInfoTableData.usgCd, catalog.MS_SWAGE_PPL_USE_CODES)})`
|
||||
: ""}{" "}
|
||||
정보 <br />
|
||||
관리번호 : {displayManagementNo || "신규"}
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import type { GisEditSessionSnapshot, GisEditTool } from "./gisEditTypes";
|
||||
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
|
||||
import type { GisEditSessionSnapshot, GisEditTool } from "./gis-edit-types";
|
||||
import { useGisEditOperationLabels } from "./use-gis-edit-common-codes";
|
||||
|
||||
type Props = GisEditSessionSnapshot & {
|
||||
onRemoveFocusedChange: () => void;
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
import { Check, Move, PenTool, Plus, Trash2, X } from "lucide-react";
|
||||
import { Button } from "@common/components/ui";
|
||||
|
||||
import type { GisEditTool } from "./gisEditTypes";
|
||||
import { useGisEditOperationLabels } from "./useGisEditCommonCodes";
|
||||
import type { GisEditTool } from "./gis-edit-types";
|
||||
import { useGisEditOperationLabels } from "./use-gis-edit-common-codes";
|
||||
|
||||
type Props = {
|
||||
isEditMode: boolean;
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
/** GIS 편집 모델은 기능이 소유하고 기존 화면 import 경로는 유지한다. */
|
||||
export * from "@/features/gisEdit/model/types";
|
||||
export * from "@/features/gis-edit/model/types";
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
import { getCommonCodes } from "@/entities/code/api/common-code-api";
|
||||
|
||||
import type { GisEditTool } from "./gisEditTypes";
|
||||
import type { GisEditTool } from "./gis-edit-types";
|
||||
|
||||
const GIS_EDIT_OPERATION_UP_CD = "GISEDITOPRT";
|
||||
|
||||
+3
-3
@@ -22,15 +22,15 @@ import {
|
||||
getFeatureProperties,
|
||||
normalizePipeGeometry,
|
||||
createChangeId,
|
||||
} from "@/features/gisEdit";
|
||||
export { EMPTY_GIS_EDIT_PIPE_BASIC_INFO } from "@/features/gisEdit";
|
||||
} from "@/features/gis-edit";
|
||||
export { EMPTY_GIS_EDIT_PIPE_BASIC_INFO } from "@/features/gis-edit";
|
||||
|
||||
import type {
|
||||
GisEditChange,
|
||||
GisEditPipeBasicInfo,
|
||||
GisEditSavePayload,
|
||||
GisEditTool,
|
||||
} from "./gisEditTypes";
|
||||
} from "./gis-edit-types";
|
||||
|
||||
export const useGisEdit = (treeData: any[]) => {
|
||||
const editableLayers = useMemo(() => collectEditableLayers(treeData), [treeData]);
|
||||
+13
-17
@@ -12,26 +12,22 @@ import {
|
||||
} from "@common/business_components";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
import {
|
||||
getBlobDataWithHeaders,
|
||||
getData,
|
||||
postMutiPart,
|
||||
postRawJson,
|
||||
} from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import { getBlobDataWithHeaders, postMutiPart } from "@/shared/api/file-api";
|
||||
import { getData, postRawJson } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import GisUploadPreviewMap from "./GisUploadPreviewMap";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { useGisManagementCommonCodes } from "./useGisManagementCommonCodes";
|
||||
import type { UploadFacilityType, MngInstOption, UploadHistoryItem, GisUploadResponse, ValidationErrorItem, ViewMode } from "@/features/gisManagement/model/upload-types";
|
||||
import { getLocalizedValidationErrorMessage, getFileExtension, formatDateTime, toScopeKey, buildManagementLabel, getErrorMessage } from "@/features/gisManagement/lib/upload-display";
|
||||
import { createGisDownloadClient, saveBlob } from "@/features/gisManagement/api/gis-download";
|
||||
import { postRefreshMessageToOpener } from "@/features/gisManagement/lib/popup-refresh";
|
||||
import GisUploadPreviewMap from "./gis-upload-preview-map";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { useGisManagementCommonCodes } from "./use-gis-management-common-codes";
|
||||
import type { UploadFacilityType, MngInstOption, UploadHistoryItem, GisUploadResponse, ValidationErrorItem, ViewMode } from "@/features/gis-management/model/upload-types";
|
||||
import { getLocalizedValidationErrorMessage, getFileExtension, formatDateTime, toScopeKey, buildManagementLabel, getErrorMessage } from "@/features/gis-management/lib/upload-display";
|
||||
import { createGisDownloadClient, saveBlob } from "@/features/gis-management/api/gis-download";
|
||||
import { postRefreshMessageToOpener } from "@/features/gis-management/lib/popup-refresh";
|
||||
|
||||
import { createGisUploadRetryGuard, getGisPartialSuccessNotice } from "@/features/gisManagement";
|
||||
import { createGisUploadRetryGuard, getGisPartialSuccessNotice } from "@/features/gis-management";
|
||||
|
||||
// 기존 전송 경로를 유지하며 다운로드 작업의 비동기 처리는 기능 모듈에 위임한다.
|
||||
const { downloadGeoPackage, startDownloadJob, waitForDownloadJob } =
|
||||
+1
-1
@@ -20,7 +20,7 @@ import { get as getProjection } from "ol/proj";
|
||||
import { register } from "ol/proj/proj4";
|
||||
import proj4 from "proj4";
|
||||
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
|
||||
type GisUploadPreviewMapProps = {
|
||||
uploadId: string;
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
import { getCommonCodes } from "@/entities/code/api/common-code-api";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
|
||||
+9
-9
@@ -15,21 +15,21 @@ import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import useStore from "@/store/useStore";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
|
||||
import {
|
||||
INFTWT_RESULT_ROW_SELECT_MESSAGE,
|
||||
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
|
||||
type InftwtResultRow,
|
||||
} from "./inftwtResultTypes";
|
||||
} from "./inftwt-result-types";
|
||||
|
||||
const INFTWT_RESULT_PAGE_SIZE = 10;
|
||||
|
||||
+3
-3
@@ -10,8 +10,8 @@ import Fill from "ol/style/Fill.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import { intersects as intersectsExtent } from "ol/extent";
|
||||
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
|
||||
type InftwtResultMapViewResponse,
|
||||
type InftwtResultRow,
|
||||
} from "./inftwtResultTypes";
|
||||
} from "./inftwt-result-types";
|
||||
|
||||
type UseInftwtResultMapParams = {
|
||||
gisMaps: MutableRefObject<any | null>;
|
||||
+47
-21
@@ -9,15 +9,15 @@ import { useSearchParams } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { requestGridExcel } from "@/shared/api/grid-export";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import {
|
||||
PIPE_ANALYSIS_DETAIL_DEFAULT_FILTERS,
|
||||
PIPE_ANALYSIS_DETAIL_HEADLIST,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
toPipeAnalysisDetailViewRows,
|
||||
type PipeAnalysisDetailFilters,
|
||||
type PipeAnalysisDetailRow,
|
||||
} from "./pipeAnalysisUtils";
|
||||
} from "./pipe-analysis-utils";
|
||||
|
||||
type DetailViewRow = ReturnType<typeof toPipeAnalysisDetailViewRows>[number];
|
||||
const PIPE_ANALYSIS_DETAIL_PAGE_SIZE = 10;
|
||||
@@ -47,6 +47,8 @@ const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
|
||||
: fallbackMessage;
|
||||
|
||||
export default function PipeAnalysisDetailPopupPage() {
|
||||
const { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } =
|
||||
useFacilityCodeCatalog();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const searchParams = useSearchParams();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
@@ -143,11 +145,20 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
responseRows as PipeAnalysisDetailRow[],
|
||||
ctpv,
|
||||
sgg,
|
||||
{ materials: MS_SWAGE_PPL_CODES, uses: MS_SWAGE_PPL_USE_CODES },
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[baseAnalysisType, baseElapsedYearBucket, ctpv, scope.regionCode, sgg],
|
||||
[
|
||||
baseAnalysisType,
|
||||
baseElapsedYearBucket,
|
||||
ctpv,
|
||||
scope.regionCode,
|
||||
sgg,
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
],
|
||||
);
|
||||
|
||||
const loadRows = useCallback(
|
||||
@@ -227,17 +238,29 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
setIsLoading(true);
|
||||
const params = new URLSearchParams({ analysisType: baseAnalysisType });
|
||||
appendApiSearchParams(params, {
|
||||
ctpvCd: appliedFilters.ctpvCd, sggCd: appliedFilters.sggCd,
|
||||
swtrmtZoneNm: appliedFilters.swtrmtZoneNm, elapsedYearBucket: baseElapsedYearBucket,
|
||||
materialCode: appliedFilters.materialCode, useCd: appliedFilters.useCd,
|
||||
diameterFrom: appliedFilters.diameterFrom, diameterTo: appliedFilters.diameterTo,
|
||||
ctpvCd: appliedFilters.ctpvCd,
|
||||
sggCd: appliedFilters.sggCd,
|
||||
swtrmtZoneNm: appliedFilters.swtrmtZoneNm,
|
||||
elapsedYearBucket: baseElapsedYearBucket,
|
||||
materialCode: appliedFilters.materialCode,
|
||||
useCd: appliedFilters.useCd,
|
||||
diameterFrom: appliedFilters.diameterFrom,
|
||||
diameterTo: appliedFilters.diameterTo,
|
||||
});
|
||||
if (scope.regionCode) params.set("scopeRegionCode", scope.regionCode);
|
||||
await requestGridExcel("v1/spatialGridExports/pipes", {
|
||||
pipe: Object.fromEntries(params),
|
||||
ctpvNames: Object.fromEntries(ctpv.map((item) => [String(item.val).trim(), item.text])),
|
||||
sggNames: Object.fromEntries(sgg.map((item) => [String(item.val).trim(), item.text])),
|
||||
}, `관로분석_관로현황_${buildPipeAnalysisFileDate()}.xlsx`);
|
||||
await requestGridExcel(
|
||||
"v1/spatialGridExports/pipes",
|
||||
{
|
||||
pipe: Object.fromEntries(params),
|
||||
ctpvNames: Object.fromEntries(
|
||||
ctpv.map((item) => [String(item.val).trim(), item.text]),
|
||||
),
|
||||
sggNames: Object.fromEntries(
|
||||
sgg.map((item) => [String(item.val).trim(), item.text]),
|
||||
),
|
||||
},
|
||||
`관로분석_관로현황_${buildPipeAnalysisFileDate()}.xlsx`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("관로현황 다운로드 중 오류:", error);
|
||||
alert(
|
||||
@@ -264,12 +287,12 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
) {
|
||||
options.push({
|
||||
value: baseMaterialCode,
|
||||
label: resolveMaterialName(baseMaterialCode),
|
||||
label: resolveMaterialName(baseMaterialCode, MS_SWAGE_PPL_CODES),
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [baseMaterialCode]);
|
||||
}, [baseMaterialCode, MS_SWAGE_PPL_CODES]);
|
||||
|
||||
const titleSuffix = useMemo(() => {
|
||||
const locationParts: string[] = [];
|
||||
@@ -300,7 +323,7 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
if (baseElapsedYearBucket)
|
||||
parts.push(resolveElapsedYearBucketLabel(baseElapsedYearBucket));
|
||||
if (baseMaterialCode && baseMaterialCode !== "0")
|
||||
parts.push(resolveMaterialName(baseMaterialCode));
|
||||
parts.push(resolveMaterialName(baseMaterialCode, MS_SWAGE_PPL_CODES));
|
||||
return parts.length > 0 ? ` (${parts.join(" / ")})` : "";
|
||||
}, [
|
||||
baseAnalysisType,
|
||||
@@ -310,6 +333,7 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
filters.ctpvCd,
|
||||
filters.sggCd,
|
||||
filters.swtrmtZoneNm,
|
||||
MS_SWAGE_PPL_CODES,
|
||||
rows,
|
||||
sgg,
|
||||
]);
|
||||
@@ -385,7 +409,9 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
if (event.key === "Enter") handleSearch();
|
||||
}}
|
||||
/>
|
||||
<span className="shrink-0 text-xl font-semibold text-foreground">~</span>
|
||||
<span className="shrink-0 text-xl font-semibold text-foreground">
|
||||
~
|
||||
</span>
|
||||
<Input
|
||||
className="sn-popup-input !h-[3.2rem] w-[12rem] !text-xl"
|
||||
aria-label="관경 최대값"
|
||||
+8
-8
@@ -8,16 +8,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import PipeAnalysisSummaryTable from "./PipeAnalysisSummaryTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import PipeAnalysisSummaryTable from "./pipe-analysis-summary-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import {
|
||||
PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS,
|
||||
PIPE_ANALYSIS_TYPE_OPTIONS,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type PipeAnalysisSummaryResponse,
|
||||
type PipeAnalysisSummaryViewRow,
|
||||
type PipeAnalysisYearMaterialGroup,
|
||||
} from "./pipeAnalysisUtils";
|
||||
} from "./pipe-analysis-utils";
|
||||
|
||||
const SUMMARY_FILE_PREFIX: Record<string, string> = {
|
||||
YEAR: "관로분석_경과년도별",
|
||||
+1
-1
@@ -15,7 +15,7 @@ import type {
|
||||
PipeAnalysisSummaryViewRow,
|
||||
PipeAnalysisYearMaterialGroup,
|
||||
PipeAnalysisYearMaterialGroupRow,
|
||||
} from "./pipeAnalysisUtils";
|
||||
} from "./pipe-analysis-utils";
|
||||
|
||||
type SummaryRow =
|
||||
| {
|
||||
+20
-14
@@ -3,7 +3,10 @@
|
||||
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from "../common/table";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
} from "@/entities/facility/model/facility-code-fallbacks";
|
||||
|
||||
export const PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE =
|
||||
"SN_PIPE_ANALYSIS_DETAIL_ROW_SELECT";
|
||||
@@ -125,13 +128,6 @@ export type PipeAnalysisYearMaterialGroup = {
|
||||
rows: PipeAnalysisYearMaterialGroupRow[];
|
||||
};
|
||||
|
||||
const materialMap = new Map(
|
||||
MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]),
|
||||
);
|
||||
const useMap = new Map(
|
||||
MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]),
|
||||
);
|
||||
|
||||
export const PIPE_ANALYSIS_TYPE_OPTIONS: Array<{
|
||||
value: PipeAnalysisType;
|
||||
label: string;
|
||||
@@ -486,13 +482,17 @@ export const toPipeAnalysisDetailViewRows = (
|
||||
rows: PipeAnalysisDetailRow[],
|
||||
ctpvOptions: SelectDataType[],
|
||||
sggOptions: SelectDataType[],
|
||||
catalog?: {
|
||||
materials: { val: string; text: string }[];
|
||||
uses: { val: string; text: string }[];
|
||||
},
|
||||
) =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
ctpvNm: row.ctpvNm || resolveRegionName(row.ctpvCd, ctpvOptions, 2),
|
||||
sggNm: row.sggNm || resolveRegionName(row.sggCd, sggOptions, 5),
|
||||
useNm: resolveUseName(row.useCd),
|
||||
materialName: resolveMaterialName(row.materialCode),
|
||||
useNm: resolveUseName(row.useCd, catalog?.uses),
|
||||
materialName: resolveMaterialName(row.materialCode, catalog?.materials),
|
||||
}));
|
||||
|
||||
export const buildPipeAnalysisSummaryDownloadHeaders = (
|
||||
@@ -538,11 +538,14 @@ export const PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS = {
|
||||
materialName: "관재질",
|
||||
};
|
||||
|
||||
export const resolveMaterialName = (materialCode?: string) => {
|
||||
export const resolveMaterialName = (
|
||||
materialCode?: string,
|
||||
codes = MS_SWAGE_PPL_CODES,
|
||||
) => {
|
||||
const code = String(materialCode ?? "").trim();
|
||||
if (!code) return "-";
|
||||
if (code === "OTHER") return "기타";
|
||||
return materialMap.get(code) ?? code;
|
||||
return codes.find((item) => item.val.trim() === code)?.text ?? code;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -627,10 +630,13 @@ export const resolvePipeAnalysisTypeLabel = (analysisType?: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const resolveUseName = (useCode?: string) => {
|
||||
export const resolveUseName = (
|
||||
useCode?: string,
|
||||
codes = MS_SWAGE_PPL_USE_CODES,
|
||||
) => {
|
||||
const code = String(useCode ?? "").trim();
|
||||
if (!code) return "-";
|
||||
return useMap.get(code) ?? code;
|
||||
return codes.find((item) => item.val.trim() === code)?.text ?? code;
|
||||
};
|
||||
|
||||
export const resolveRegionName = (
|
||||
+9
-9
@@ -13,23 +13,23 @@ import {
|
||||
} from "@common/business_components";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
import { downloadAnalysisExcel } from "@/shared/lib/download-analysis-excel";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import useStore from "@/store/useStore";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
|
||||
import {
|
||||
RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
|
||||
type RdiiResultRow,
|
||||
} from "./rdiiResultTypes";
|
||||
} from "./rdii-result-types";
|
||||
|
||||
const RDII_RESULT_PAGE_SIZE = 10;
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ import VectorSource from "ol/source/Vector.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
|
||||
type RdiiResultMapViewResponse,
|
||||
type RdiiResultRow,
|
||||
} from "./rdiiResultTypes";
|
||||
} from "./rdii-result-types";
|
||||
|
||||
type UseRdiiResultMapParams = {
|
||||
gisMaps: MutableRefObject<any | null>;
|
||||
+33
-16
@@ -11,23 +11,23 @@ import {
|
||||
type UiHeadProps,
|
||||
} from "@common/business_components";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import {
|
||||
WINDOW_POPUP_MESSAGE_TYPE,
|
||||
type WindowPopupContentProps,
|
||||
type WindowPopupMessageType,
|
||||
} from "@/features/windowPopup/model/types";
|
||||
import { MS_SWAGE_PPL_CODES } from "../common/table";
|
||||
} from "@/features/window-popup/model/types";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import {
|
||||
downloadRepairHistoriesByZoneId,
|
||||
fetchRepairHistoryPage,
|
||||
fetchRepairHistoryMapFeatures,
|
||||
type RepairHistoryFilters,
|
||||
type RepairHistoryRow,
|
||||
} from "../popupComponent/swtrmtPrcsZoneAnalysisApi";
|
||||
} from "@/features/facility-detail/api/treatment-zone-api";
|
||||
|
||||
const REPAIR_HISTORY_PAGE_SIZE = 10;
|
||||
const MAP_LAYER_SETTING_PATH =
|
||||
@@ -163,6 +163,7 @@ const formatYear = (value: unknown) => {
|
||||
export default function RepairHistoryPopupPage({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const { MS_SWAGE_PPL_CODES } = useFacilityCodeCatalog();
|
||||
const zoneId = useMemo(
|
||||
() => firstSearchParam(searchParams.zoneId),
|
||||
[searchParams.zoneId],
|
||||
@@ -201,13 +202,24 @@ export default function RepairHistoryPopupPage({
|
||||
const requestId = ++pageRequestId.current;
|
||||
if (refreshMap) {
|
||||
const mapId = ++mapRequestId.current;
|
||||
postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: [] });
|
||||
void fetchRepairHistoryMapFeatures(zoneId, nextFilters).then((features) => {
|
||||
if (mapId !== mapRequestId.current) return;
|
||||
postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: features });
|
||||
}).catch(() => {
|
||||
if (mapId === mapRequestId.current) setErrorMessage("보수이력 지도 도형을 불러오지 못했습니다. 조회를 다시 실행해주세요.");
|
||||
});
|
||||
postRepairMessage(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
|
||||
{ zoneId, rows: [] },
|
||||
);
|
||||
void fetchRepairHistoryMapFeatures(zoneId, nextFilters)
|
||||
.then((features) => {
|
||||
if (mapId !== mapRequestId.current) return;
|
||||
postRepairMessage(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
|
||||
{ zoneId, rows: features },
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (mapId === mapRequestId.current)
|
||||
setErrorMessage(
|
||||
"보수이력 지도 도형을 불러오지 못했습니다. 조회를 다시 실행해주세요.",
|
||||
);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const response = await fetchRepairHistoryPage(
|
||||
@@ -228,7 +240,10 @@ export default function RepairHistoryPopupPage({
|
||||
setTotal(0);
|
||||
if (refreshMap) {
|
||||
mapRequestId.current += 1;
|
||||
postRepairMessage(WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS, { zoneId, rows: [] });
|
||||
postRepairMessage(
|
||||
WINDOW_POPUP_MESSAGE_TYPE.REPAIR_HISTORY_SHOW_RESULTS,
|
||||
{ zoneId, rows: [] },
|
||||
);
|
||||
}
|
||||
setErrorMessage(
|
||||
error instanceof Error
|
||||
@@ -468,7 +483,9 @@ export default function RepairHistoryPopupPage({
|
||||
total={total}
|
||||
page={currentPage}
|
||||
pageSize={REPAIR_HISTORY_PAGE_SIZE}
|
||||
onPageChange={(page) => void searchRepairHistories(appliedFilters, page, false)}
|
||||
onPageChange={(page) =>
|
||||
void searchRepairHistories(appliedFilters, page, false)
|
||||
}
|
||||
onRowClick={(_, index) => {
|
||||
const row = rows[index];
|
||||
if (row) handleRowClick(row);
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/** 우측 상세 탭의 표시·분류 모델을 지도 조회 수명에서 분리한다. */
|
||||
import type { ExmnMapJobType } from "../exmn/exmnMapTypes";
|
||||
import type { SinkholeDetail, SinkholeNearbyPipe } from "../sinkhole/sinkholeTypes";
|
||||
import type { ExmnMapJobType } from "../exmn/exmn-map-types";
|
||||
import type { SinkholeDetail, SinkholeNearbyPipe } from "../sinkhole/sinkhole-types";
|
||||
|
||||
export type RightPanelTabKind = "feature" | "sinkhole" | "gisEdit";
|
||||
export type RightPanelTabCategory =
|
||||
+7
-7
@@ -7,13 +7,13 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import {
|
||||
SBMRG_DNGR_MAP_DEFAULT_FILTERS,
|
||||
SBMRG_DNGR_MAP_FREQUENCY_OPTIONS,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
type SbmrgDngrMapApiItem,
|
||||
type SbmrgDngrMapFilters,
|
||||
type SbmrgDngrMapTableRow,
|
||||
} from "./sbmrgDngrMapTypes";
|
||||
} from "./sbmrg-dngr-map-types";
|
||||
|
||||
const unwrapPayload = <T,>(response: any): T =>
|
||||
(response?.data ?? response) as T;
|
||||
+4
-4
@@ -6,10 +6,10 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { getCmsData } from "@/components/lib/api";
|
||||
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkholeTypes";
|
||||
import { useSinkholeCommonCodes } from "./useSinkholeCommonCodes";
|
||||
import { VerticalTable } from "@/components/ui/vertical-table";
|
||||
import { getCmsData } from "@/shared/api/external-service-api";
|
||||
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkhole-types";
|
||||
import { useSinkholeCommonCodes } from "./use-sinkhole-common-codes";
|
||||
|
||||
type Props = {
|
||||
detail: SinkholeDetail;
|
||||
+2
-2
@@ -13,8 +13,8 @@ import {
|
||||
validateSinkholeYearRange,
|
||||
type SinkholeSyncSummary,
|
||||
type SinkholeYearRange,
|
||||
} from "./sinkholeTypes";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
} from "./sinkhole-types";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
type MaybePromise<T = void> = T | Promise<T>;
|
||||
|
||||
+7
-7
@@ -11,17 +11,17 @@ import {
|
||||
StandardPopupLayout,
|
||||
type UiHeadProps,
|
||||
} from "@common/business_components";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel, {
|
||||
PopupSearchActions,
|
||||
PopupSearchField,
|
||||
} from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
} from "@/features/window-popup/ui/popup-search-panel";
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import {
|
||||
buildSinkholeDefaultFilters,
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
SINKHOLE_ROW_SELECT_MESSAGE,
|
||||
type SinkholeFilters,
|
||||
type SinkholeListItem,
|
||||
} from "./sinkholeTypes";
|
||||
import { useSinkholeCommonCodes } from "./useSinkholeCommonCodes";
|
||||
} from "./sinkhole-types";
|
||||
import { useSinkholeCommonCodes } from "./use-sinkhole-common-codes";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const DOWNLOAD_BATCH_SIZE = 2000;
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getCommonCodes } from "@/components/lib/common/apiHelpers";
|
||||
import { getCommonCodes } from "@/entities/code/api/common-code-api";
|
||||
|
||||
import {
|
||||
SINKHOLE_COORD_STATUS_OPTIONS,
|
||||
SINKHOLE_RESTORE_STATUS_OPTIONS,
|
||||
} from "./sinkholeTypes";
|
||||
} from "./sinkhole-types";
|
||||
|
||||
type CodeLabelMap = Record<string, string>;
|
||||
type StatusOption = { value: string; label: string };
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/** 통수위험 분석 대상과 결과를 조회하는 API 함수를 제공한다. */
|
||||
import { getData } from "@/app/components/lib/api";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import type { UiTableSort } from "@common/business_components/ui/table";
|
||||
|
||||
import type {
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
WaterRiskResultPage,
|
||||
WaterRiskResultRow,
|
||||
WaterRiskStatus,
|
||||
} from "./waterRiskTypes";
|
||||
} from "./water-risk-types";
|
||||
|
||||
export type WaterRiskResultQuery = {
|
||||
startDate: string;
|
||||
+7
-7
@@ -6,13 +6,13 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import {
|
||||
WINDOW_POPUP_MESSAGE_TYPE,
|
||||
type WindowPopupContentProps,
|
||||
} from "@/features/windowPopup/model/types";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
} from "@/features/window-popup/model/types";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import {
|
||||
StandardPopupLayout,
|
||||
type UiHeadProps,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
fetchWaterRiskAreas,
|
||||
fetchWaterRiskMapView,
|
||||
fetchWaterRiskResults,
|
||||
} from "./waterRiskClient";
|
||||
} from "./water-risk-client";
|
||||
import {
|
||||
buildWaterRiskStatusLabel,
|
||||
EMPTY_WATER_RISK_SESSION_SNAPSHOT,
|
||||
@@ -33,9 +33,9 @@ import {
|
||||
type WaterRiskResultPage,
|
||||
type WaterRiskResultRow,
|
||||
type WaterRiskSessionSnapshot,
|
||||
} from "./waterRiskTypes";
|
||||
} from "./water-risk-types";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
|
||||
type WaterRiskResultMode = "current" | "history";
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/** 조사결과·침수이력의 지도 스타일. 기존 값과 처리 순서를 유지한다. */
|
||||
import Fill from "ol/style/Fill.js";
|
||||
import Icon from "ol/style/Icon.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Text from "ol/style/Text.js";
|
||||
import {
|
||||
EXMN_GRADE_COLORS,
|
||||
EXMN_RESULT_MANHOLE_ICON_SRC,
|
||||
} from "../model/map-constants";
|
||||
|
||||
export const getExmnGradeColor = (feature: any) => {
|
||||
const grade = Number(feature?.get?.("structuralGrade") ?? 0);
|
||||
return EXMN_GRADE_COLORS[grade] ?? "#2563eb";
|
||||
};
|
||||
|
||||
export const createExmnPipeStyle = (feature: any) =>
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: getExmnGradeColor(feature),
|
||||
width: 4,
|
||||
}),
|
||||
});
|
||||
|
||||
// 레이어 트리 범례와 지도 피처가 동일한 기존 맨홀 형태의 보라색 심볼을 사용한다.
|
||||
export const createExmnManholeStyle = () =>
|
||||
new Style({
|
||||
image: new Icon({
|
||||
src: EXMN_RESULT_MANHOLE_ICON_SRC,
|
||||
anchor: [0.5, 0.5],
|
||||
anchorXUnits: "fraction",
|
||||
anchorYUnits: "fraction",
|
||||
}),
|
||||
});
|
||||
|
||||
export const createFldngHstryAreaStyle = (label: string) =>
|
||||
new Style({
|
||||
fill: new Fill({
|
||||
color: "rgba(30, 136, 229, 0.16)",
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: "rgba(30, 136, 229, 0.95)",
|
||||
width: 3,
|
||||
}),
|
||||
text: new Text({
|
||||
text: label,
|
||||
font: "700 12px sans-serif",
|
||||
fill: new Fill({ color: "#ffffff" }),
|
||||
backgroundFill: new Fill({ color: "rgba(15, 23, 42, 0.88)" }),
|
||||
padding: [4, 7, 4, 7],
|
||||
overflow: true,
|
||||
}),
|
||||
zIndex: 9994,
|
||||
});
|
||||
|
||||
export const createFldngHstryDepthStyle = (label: string, color: string) =>
|
||||
new Style({
|
||||
fill: new Fill({
|
||||
color,
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: "rgba(71, 85, 105, 0.72)",
|
||||
width: 1.4,
|
||||
}),
|
||||
text: new Text({
|
||||
text: label,
|
||||
font: "700 11px sans-serif",
|
||||
fill: new Fill({ color: "#0f172a" }),
|
||||
overflow: true,
|
||||
}),
|
||||
zIndex: 9993,
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/** GIS 편집 FID·속성·선택 변환 보조. 기존 값과 처리 순서를 유지한다. */
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import type { GisEditSessionSnapshot } from "@/app/(map-layout)/map/components/gis-edit/gis-edit-types";
|
||||
import { GIS_EDIT_PIPE_SNAP_BBOX_BUFFER_RATIO } from "../model/map-constants";
|
||||
|
||||
export const expandGisEditPipeSnapExtent = (extent: number[]) => {
|
||||
const [minX = 0, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
const width = Math.max(0, maxX - minX);
|
||||
const height = Math.max(0, maxY - minY);
|
||||
const bufferX = width * GIS_EDIT_PIPE_SNAP_BBOX_BUFFER_RATIO;
|
||||
const bufferY = height * GIS_EDIT_PIPE_SNAP_BBOX_BUFFER_RATIO;
|
||||
return [minX - bufferX, minY - bufferY, maxX + bufferX, maxY + bufferY];
|
||||
};
|
||||
|
||||
export const normalizeGisEditFid = (value: unknown) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return "";
|
||||
const withoutNamespace = raw.includes(":")
|
||||
? (raw.split(":").pop() ?? raw)
|
||||
: raw;
|
||||
return withoutNamespace.startsWith("tb_ms_swage_ppl.")
|
||||
? withoutNamespace.slice("tb_ms_swage_ppl.".length)
|
||||
: withoutNamespace;
|
||||
};
|
||||
|
||||
export const getGisEditFeatureFid = (data: any) =>
|
||||
normalizeGisEditFid(
|
||||
data?.get?.("fid") ??
|
||||
data?.values_?.fid ??
|
||||
data?.fid ??
|
||||
data?.getId?.() ??
|
||||
data?.id_ ??
|
||||
"",
|
||||
);
|
||||
|
||||
export const isGisEditPreviewFeatureData = (data: any) =>
|
||||
Boolean(String(data?.get?.("changeId") ?? data?.changeId ?? "").trim());
|
||||
|
||||
export const highlightGisEditSelectedFeature = (featureData: any) => {
|
||||
if (
|
||||
!featureData ||
|
||||
typeof featureData.getGeometry !== "function" ||
|
||||
typeof featureData.setStyle !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const select = GisApp.Module.select as any;
|
||||
select?.selectFeature?.(featureData, true);
|
||||
};
|
||||
|
||||
export const isVisibleGisEditSessionChange = (
|
||||
change: GisEditSessionSnapshot["changes"][number],
|
||||
) => !change.attributeOnly || Boolean(change.basicInfoSavedAt);
|
||||
|
||||
export const getLatestNumericEditSn = (histories: any[]) =>
|
||||
histories.reduce<number | null>((latest, history) => {
|
||||
const editSn = String(history?.editSn ?? "").trim();
|
||||
if (!/^\d+$/.test(editSn)) return latest;
|
||||
const value = Number(editSn);
|
||||
return latest === null || value > latest ? value : latest;
|
||||
}, null);
|
||||
|
||||
export const getGisEditAttributeText = (
|
||||
attributes: Record<string, unknown> | null | undefined,
|
||||
keys: string[],
|
||||
) => {
|
||||
for (const key of keys) {
|
||||
const value = String(attributes?.[key] ?? "").trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const buildGisEditHiddenPipeFilter = (fids: string[]) => {
|
||||
const normalizedFids = Array.from(
|
||||
new Set(
|
||||
fids
|
||||
.map((fid) => normalizeGisEditFid(fid))
|
||||
.filter((fid) => /^\d+$/.test(fid)),
|
||||
),
|
||||
);
|
||||
if (normalizedFids.length === 0) return null;
|
||||
return `fid NOT IN (${normalizedFids.join(",")})`;
|
||||
};
|
||||
|
||||
export const findInvalidPipeRequiredChange = (
|
||||
changes: Array<{
|
||||
changeId: string;
|
||||
editType: string;
|
||||
clientTempId?: string;
|
||||
attributes?: Record<string, unknown> | null;
|
||||
}>,
|
||||
) =>
|
||||
changes.find((change) => {
|
||||
const attributes = change.attributes ?? {};
|
||||
const hasAttributePayload = Object.keys(attributes).length > 0;
|
||||
if (change.editType !== "ADD" && !hasAttributePayload) return false;
|
||||
|
||||
const mngInstCd = getGisEditAttributeText(attributes, ["mngInstCd"]);
|
||||
const fcltMngNo = getGisEditAttributeText(attributes, ["fcltMngNo"]);
|
||||
const instMngNo = getGisEditAttributeText(attributes, ["instMngNo"]);
|
||||
const usgCd = getGisEditAttributeText(attributes, ["usgCd"]);
|
||||
return (
|
||||
!mngInstCd ||
|
||||
!fcltMngNo ||
|
||||
/^NEW-/i.test(fcltMngNo) ||
|
||||
!instMngNo ||
|
||||
/^NEW-/i.test(instMngNo) ||
|
||||
!usgCd
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/** 레이어 트리 구성과 관종별 유하방향 표시 계산. 기존 값과 처리 순서를 유지한다. */
|
||||
import { AGED_PIPE_CONTROL_NODE_ID, AGED_PIPE_HIGHLIGHT_COLOR, SINKHOLE_TREE_ICON_NAME } from "../model/map-constants";
|
||||
|
||||
|
||||
|
||||
|
||||
export const appendAgedPipeControlNode = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
const isAgedPipeControlNode = (item: any) =>
|
||||
String(item.lyrSn ?? item.id) === AGED_PIPE_CONTROL_NODE_ID ||
|
||||
item.lyrNm === "노후관로";
|
||||
|
||||
if (data.some(isAgedPipeControlNode)) {
|
||||
return data.map((item) =>
|
||||
isAgedPipeControlNode(item)
|
||||
? {
|
||||
...item,
|
||||
gobjTy: "line",
|
||||
iconNm: null,
|
||||
lnColor: AGED_PIPE_HIGHLIGHT_COLOR,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...data,
|
||||
{
|
||||
lyrSn: AGED_PIPE_CONTROL_NODE_ID,
|
||||
id: AGED_PIPE_CONTROL_NODE_ID,
|
||||
lyrNm: "노후관로",
|
||||
upperLyrSn: null,
|
||||
upId: null,
|
||||
sortOrdr: "4",
|
||||
drawOrdr: null,
|
||||
flterSql: null,
|
||||
gobjTy: "line",
|
||||
lyrTyNm: "tb_old_pipe",
|
||||
useYn: "Y",
|
||||
minViewSize: null,
|
||||
iconNm: null,
|
||||
iconSize: null,
|
||||
lblAtrbNm: null,
|
||||
fontNm: null,
|
||||
fontIntnColor: null,
|
||||
fontExtnColor: null,
|
||||
fontExtnWdth: null,
|
||||
lnWdth: null,
|
||||
lnColor: AGED_PIPE_HIGHLIGHT_COLOR,
|
||||
polgnColor: null,
|
||||
initEprssYn: "Y",
|
||||
clickPsblYn: "N",
|
||||
exprsYn: "Y",
|
||||
cmncNm: "all",
|
||||
srvcNm: "wfs",
|
||||
actlLyrYn: "N",
|
||||
clckZoom: null,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const normalizeTreeRowSpacing = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
|
||||
return data.map((item) => {
|
||||
if (item?.lyrNm !== "지반침하정보") {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
iconNm: item.iconNm || SINKHOLE_TREE_ICON_NAME,
|
||||
gobjTy: "",
|
||||
};
|
||||
});
|
||||
};
|
||||
export { extractUsageCodesFromLayerInfo, isLayerVisibleAtZoom, extractCheckedPipeUsageStyles } from "@/features/map-layers/lib/flow-direction-selection";
|
||||
@@ -0,0 +1,26 @@
|
||||
/** 지도 화면의 응답·날짜·키보드 입력 판별. 기존 값과 처리 순서를 유지한다. */
|
||||
|
||||
/** 침수예측 오버레이용 12자리 분석시각을 화면 표시 형식으로 변환한다. */
|
||||
export const formatFloodPredictionDateTime = (value: unknown) => {
|
||||
const text = String(value ?? "").trim();
|
||||
return /^\d{12}$/.test(text)
|
||||
? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)} ${text.slice(8, 10)}:${text.slice(10, 12)}`
|
||||
: text || "-";
|
||||
};
|
||||
|
||||
|
||||
export const geometryObjectToKey = (
|
||||
geometry: Record<string, unknown> | null,
|
||||
) => (geometry ? JSON.stringify(geometry) : "");
|
||||
|
||||
export const isTypingElement = (target: EventTarget | null) => {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
const tagName = target.tagName.toLowerCase();
|
||||
return (
|
||||
tagName === "input" ||
|
||||
tagName === "textarea" ||
|
||||
tagName === "select" ||
|
||||
target.isContentEditable
|
||||
);
|
||||
};
|
||||
export { getFeatureCollection } from "@/features/map-layers/lib/geojson-response";
|
||||
@@ -0,0 +1,146 @@
|
||||
/** 시설 선택 결과의 우측 탭 식별·표시 변환. 기존 값과 처리 순서를 유지한다. */
|
||||
import { type RightPanelTabCategory } from "@/app/(map-layout)/map/components/right-panel/right-panel-bookmark-tabs";
|
||||
import { FLDNG_HSTRY_LAYER_NAME } from "@/entities/flood-history/model/flood-history-types";
|
||||
|
||||
export const getSelectedFeatureLayerTypeName = (data: any) => {
|
||||
const previewLayerId = String(data?.get?.("layerId") ?? "").trim();
|
||||
if (String(data?.get?.("changeId") ?? "").trim() && previewLayerId) {
|
||||
return previewLayerId;
|
||||
}
|
||||
|
||||
if (typeof data?.getId === "function") {
|
||||
return String(data.getId() ?? "").split(".")[0];
|
||||
}
|
||||
|
||||
if (typeof data?.id_ === "string") {
|
||||
return String(data.id_).split(".")[0];
|
||||
}
|
||||
|
||||
return String(data?.lyrTypeNm ?? data?.values_?.lyrTypeNm ?? "").trim();
|
||||
};
|
||||
|
||||
export const getRightPanelFeatureIdentifier = (data: any) => {
|
||||
const rawId =
|
||||
data?.get?.("changeId") ??
|
||||
data?.get?.("fid") ??
|
||||
data?.values_?.fid ??
|
||||
data?.fid ??
|
||||
data?.getId?.() ??
|
||||
data?.id_ ??
|
||||
data?.fcltMngNo ??
|
||||
data?.values_?.fclt_mng_no ??
|
||||
data?.swtrmtFcltCd ??
|
||||
data?.values_?.swtrmt_fclt_cd ??
|
||||
"";
|
||||
|
||||
return String(rawId).trim();
|
||||
};
|
||||
|
||||
export const getRightPanelFeatureTitle = (data: any) => {
|
||||
const layerTypeName = getSelectedFeatureLayerTypeName(data);
|
||||
|
||||
switch (layerTypeName) {
|
||||
case "tb_ms_swage_ppl":
|
||||
return "하수관로";
|
||||
case "tb_ms_swage_mhl":
|
||||
return "하수맨홀";
|
||||
case "tb_ms_swage_pmstn":
|
||||
return "펌프장";
|
||||
case "tb_ms_stofcbr":
|
||||
return "우수토실";
|
||||
case "tb_il_swage_prcs_fclt":
|
||||
return "처리시설";
|
||||
case "tb_ms_swage_prcs_zone":
|
||||
return "처리구역";
|
||||
case "tb_ms_fldng_wtlv_map":
|
||||
return FLDNG_HSTRY_LAYER_NAME;
|
||||
case "tb_ms_fldng_dpth_map":
|
||||
return "침수흔적심선";
|
||||
case "om_dt_untrt_swg_rain":
|
||||
return "중점우수";
|
||||
default:
|
||||
return (
|
||||
String(data?.lyrNm ?? data?.values_?.lyrNm ?? "상세").trim() || "상세"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// OpenLayers Feature와 plain object 양쪽에서 탭 표시값 후보를 같은 순서로 읽는다.
|
||||
export const getRightPanelFeatureProperty = (data: any, keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data?.get?.(key) ?? data?.values_?.[key] ?? data?.[key];
|
||||
const normalizedValue = String(value ?? "").trim();
|
||||
if (normalizedValue) return normalizedValue;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getRightPanelTabCategory = (data: any): RightPanelTabCategory => {
|
||||
const surveyOverlayType = getRightPanelFeatureProperty(data, [
|
||||
"surveyOverlayType",
|
||||
]);
|
||||
if (surveyOverlayType === "EXMN_RESULT") return "exmnSurvey";
|
||||
const layerTypeName = getSelectedFeatureLayerTypeName(data);
|
||||
if (layerTypeName === "tb_ms_swage_ppl") return "pipe";
|
||||
if (layerTypeName === "tb_il_swage_prcs_fclt") return "treatmentFacility";
|
||||
if (layerTypeName === "tb_ms_swage_prcs_zone") return "treatmentZone";
|
||||
return "defaultFeature";
|
||||
};
|
||||
|
||||
export const getRightPanelFeatureDisplayLabel = (
|
||||
data: any,
|
||||
category: RightPanelTabCategory,
|
||||
) => {
|
||||
if (category === "exmnSurvey") {
|
||||
const managementNo =
|
||||
getRightPanelFeatureProperty(data, [
|
||||
"gisManagementNo",
|
||||
"fclt_mng_no",
|
||||
"fcltMngNo",
|
||||
]) || "-";
|
||||
const jobType = getRightPanelFeatureProperty(data, ["jobType"]);
|
||||
const surveyLabel =
|
||||
jobType === "PPL"
|
||||
? "관로조사"
|
||||
: jobType === "MNHL"
|
||||
? "맨홀조사"
|
||||
: "조사관리";
|
||||
return `${surveyLabel}(${managementNo})`;
|
||||
}
|
||||
|
||||
if (category === "pipe") {
|
||||
const managementNo =
|
||||
getRightPanelFeatureProperty(data, [
|
||||
"fclt_mng_no",
|
||||
"fcltMngNo",
|
||||
"pipeId",
|
||||
"fid",
|
||||
]) ||
|
||||
getRightPanelFeatureIdentifier(data) ||
|
||||
"-";
|
||||
return `관로(${managementNo})`;
|
||||
}
|
||||
|
||||
if (category === "treatmentFacility") {
|
||||
const facilityName =
|
||||
getRightPanelFeatureProperty(data, ["swtrmt_fclt_nm", "swtrmtFcltNm"]) ||
|
||||
getRightPanelFeatureProperty(data, ["swtrmt_fclt_cd", "swtrmtFcltCd"]) ||
|
||||
"-";
|
||||
return `처리시설(${facilityName})`;
|
||||
}
|
||||
|
||||
if (category === "treatmentZone") {
|
||||
const zoneName =
|
||||
getRightPanelFeatureProperty(data, ["swtrmt_zone_nm", "swtrmtZoneNm"]) ||
|
||||
getRightPanelFeatureProperty(data, [
|
||||
"swtrmt_zone_sn",
|
||||
"swtrmtZoneSn",
|
||||
"swtrmt_fclt_cd",
|
||||
"swtrmtFcltCd",
|
||||
]) ||
|
||||
"-";
|
||||
return `처리구역(${zoneName})`;
|
||||
}
|
||||
|
||||
return getRightPanelFeatureTitle(data);
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
/** 지반침하 선택 데이터와 도형·표시 스타일 변환. 기존 값과 처리 순서를 유지한다. */
|
||||
import Fill from "ol/style/Fill.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Text from "ol/style/Text.js";
|
||||
import {
|
||||
SINKHOLE_CONTROL_NODE_ID,
|
||||
SINKHOLE_LAYER_NAME,
|
||||
SINKHOLE_LAYER_TYPE,
|
||||
type SinkholeDetail,
|
||||
type SinkholeListItem,
|
||||
} from "@/app/(map-layout)/map/components/sinkhole/sinkhole-types";
|
||||
import {
|
||||
SINKHOLE_POINT_PALETTE,
|
||||
SINKHOLE_TREE_ICON_NAME,
|
||||
} from "../model/map-constants";
|
||||
|
||||
// 지반침하 목록 API가 정상일 때는 x/y를 바로 Point feature로 바꿔 geometry 전용 API 장애를 우회할 수 있다.
|
||||
export const buildSinkholeFeatureCollectionFromItems = (
|
||||
items: SinkholeListItem[],
|
||||
) => ({
|
||||
type: "FeatureCollection",
|
||||
features: items
|
||||
.map((item) => {
|
||||
const x = Number(item?.x);
|
||||
const y = Number(item?.y);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "Feature",
|
||||
id: `${SINKHOLE_LAYER_TYPE}.${String(item?.acdntNo ?? "").trim()}`,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [x, y],
|
||||
},
|
||||
properties: {
|
||||
...item,
|
||||
lyrTypeNm: SINKHOLE_LAYER_TYPE,
|
||||
x,
|
||||
y,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
export type SinkholePointVariant = keyof typeof SINKHOLE_POINT_PALETTE;
|
||||
|
||||
export const resolveSinkholePointVariant = (
|
||||
_ocrnYmd?: string | null,
|
||||
): SinkholePointVariant => "blue";
|
||||
|
||||
export const getSinkholeSelectionValue = (data: any, key: string) =>
|
||||
data?.[key] ??
|
||||
data?.values_?.[key] ??
|
||||
data?.get?.(key) ??
|
||||
data?.properties?.[key];
|
||||
|
||||
export const getSinkholeAccidentId = (data: any) =>
|
||||
String(getSinkholeSelectionValue(data, "acdntNo") ?? "").trim();
|
||||
|
||||
export const getSinkholeFallbackDetailFromSelection = (
|
||||
data: any,
|
||||
): Partial<SinkholeDetail> | null => {
|
||||
const acdntNo = getSinkholeAccidentId(data);
|
||||
if (!acdntNo) return null;
|
||||
|
||||
const point = getSinkholePointFromSelection(data);
|
||||
return {
|
||||
acdntNo,
|
||||
ctpvNm: String(getSinkholeSelectionValue(data, "ctpvNm") ?? ""),
|
||||
sggNm: String(getSinkholeSelectionValue(data, "sggNm") ?? ""),
|
||||
emdNm: String(getSinkholeSelectionValue(data, "emdNm") ?? ""),
|
||||
daddr: String(getSinkholeSelectionValue(data, "daddr") ?? ""),
|
||||
fullAddress: String(
|
||||
getSinkholeSelectionValue(data, "fullAddress") ??
|
||||
getSinkholeSelectionValue(data, "daddr") ??
|
||||
"",
|
||||
),
|
||||
ocrnYmd: String(getSinkholeSelectionValue(data, "ocrnYmd") ?? ""),
|
||||
restoreStatus: String(
|
||||
getSinkholeSelectionValue(data, "restoreStatus") ?? "",
|
||||
),
|
||||
restoreCompletedYmd: String(
|
||||
getSinkholeSelectionValue(data, "restoreCompletedYmd") ?? "",
|
||||
),
|
||||
coordStatus: String(
|
||||
getSinkholeSelectionValue(data, "coordStatus") ??
|
||||
getSinkholeSelectionValue(data, "geocodeStatus") ??
|
||||
"",
|
||||
),
|
||||
geocodeStatus: String(
|
||||
getSinkholeSelectionValue(data, "geocodeStatus") ??
|
||||
getSinkholeSelectionValue(data, "coordStatus") ??
|
||||
"",
|
||||
),
|
||||
geocodeSource: String(
|
||||
getSinkholeSelectionValue(data, "geocodeSource") ?? "",
|
||||
),
|
||||
geocodeErrorMessage: String(
|
||||
getSinkholeSelectionValue(data, "geocodeErrorMessage") ?? "",
|
||||
),
|
||||
latitude: getSinkholeSelectionValue(data, "latitude") ?? null,
|
||||
longitude: getSinkholeSelectionValue(data, "longitude") ?? null,
|
||||
x: point?.x ?? getSinkholeSelectionValue(data, "x") ?? null,
|
||||
y: point?.y ?? getSinkholeSelectionValue(data, "y") ?? null,
|
||||
sizeWidth: getSinkholeSelectionValue(data, "sizeWidth") ?? null,
|
||||
sizeLength: getSinkholeSelectionValue(data, "sizeLength") ?? null,
|
||||
sizeDepth: getSinkholeSelectionValue(data, "sizeDepth") ?? null,
|
||||
detailCause: getSinkholeSelectionValue(data, "detailCause") ?? null,
|
||||
deathCount: getSinkholeSelectionValue(data, "deathCount") ?? null,
|
||||
injuryCount: getSinkholeSelectionValue(data, "injuryCount") ?? null,
|
||||
vehicleDamageCount:
|
||||
getSinkholeSelectionValue(data, "vehicleDamageCount") ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const getSinkholePointFromSelection = (
|
||||
data: any,
|
||||
): { x: number; y: number } | null => {
|
||||
const directX = Number(getSinkholeSelectionValue(data, "x"));
|
||||
const directY = Number(getSinkholeSelectionValue(data, "y"));
|
||||
if (Number.isFinite(directX) && Number.isFinite(directY)) {
|
||||
return { x: directX, y: directY };
|
||||
}
|
||||
|
||||
const geometry = data?.getGeometry?.();
|
||||
if (geometry?.getType?.() === "Point") {
|
||||
const coordinates = geometry.getCoordinates?.();
|
||||
const x = Number(coordinates?.[0]);
|
||||
const y = Number(coordinates?.[1]);
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createSinkholePointStyle = (
|
||||
variant: SinkholePointVariant = "blue",
|
||||
selected = false,
|
||||
) =>
|
||||
new Style({
|
||||
image: new CircleStyle({
|
||||
radius: selected ? 9 : 7,
|
||||
fill: new Fill({
|
||||
color: selected
|
||||
? SINKHOLE_POINT_PALETTE[variant].fillSelected
|
||||
: SINKHOLE_POINT_PALETTE[variant].fill,
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: selected
|
||||
? SINKHOLE_POINT_PALETTE[variant].strokeSelected
|
||||
: SINKHOLE_POINT_PALETTE[variant].stroke,
|
||||
width: selected ? 2.6 : 2,
|
||||
}),
|
||||
}),
|
||||
zIndex: selected ? 9997 : 9994,
|
||||
});
|
||||
|
||||
export const createSinkholeRadiusStyle = (
|
||||
variant: SinkholePointVariant = "blue",
|
||||
) =>
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: SINKHOLE_POINT_PALETTE[variant].radiusStroke,
|
||||
width: 2.2,
|
||||
lineDash: [8, 6],
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: SINKHOLE_POINT_PALETTE[variant].radiusFill,
|
||||
}),
|
||||
zIndex: 9993,
|
||||
});
|
||||
|
||||
// 반경 원 상단에 실제 적용 거리값을 표시하는 선택 전용 지도 라벨 스타일이다.
|
||||
export const createSinkholeRadiusLabelStyle = (
|
||||
radiusMeters: number,
|
||||
variant: SinkholePointVariant = "blue",
|
||||
) =>
|
||||
new Style({
|
||||
text: new Text({
|
||||
text: `반경 ${radiusMeters.toLocaleString("ko-KR")}m`,
|
||||
font: "600 13px Pretendard, sans-serif",
|
||||
textAlign: "center",
|
||||
offsetY: -14,
|
||||
padding: [4, 7, 4, 7],
|
||||
fill: new Fill({ color: SINKHOLE_POINT_PALETTE[variant].strokeSelected }),
|
||||
stroke: new Stroke({ color: "rgba(255, 255, 255, 0.98)", width: 3 }),
|
||||
backgroundFill: new Fill({ color: "rgba(255, 255, 255, 0.9)" }),
|
||||
backgroundStroke: new Stroke({
|
||||
color: SINKHOLE_POINT_PALETTE[variant].radiusStroke,
|
||||
width: 1.5,
|
||||
}),
|
||||
}),
|
||||
zIndex: 9998,
|
||||
});
|
||||
|
||||
export const createSinkholeFocusedPipeStyle = () => [
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(88, 28, 135, 0.22)",
|
||||
width: 18,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9997,
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(255, 255, 255, 0.96)",
|
||||
width: 12,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9998,
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(37, 99, 235, 0.98)",
|
||||
width: 8,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9999,
|
||||
}),
|
||||
];
|
||||
|
||||
export const appendSinkholeControlNode = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
const existingRoot = data.find((item) => {
|
||||
const nodeId = String(item?.lyrSn ?? item?.id ?? "");
|
||||
const layerName = String(item?.lyrNm ?? item?.name ?? "").trim();
|
||||
const layerTypeName = String(
|
||||
item?.lyrTyNm ?? item?.lyrTypeNm ?? item?.lyr_type_nm ?? "",
|
||||
).trim();
|
||||
return (
|
||||
nodeId === SINKHOLE_CONTROL_NODE_ID ||
|
||||
layerName === SINKHOLE_LAYER_NAME ||
|
||||
layerTypeName === SINKHOLE_LAYER_TYPE
|
||||
);
|
||||
});
|
||||
|
||||
const existingRootId =
|
||||
existingRoot?.lyrSn ?? existingRoot?.id ?? SINKHOLE_CONTROL_NODE_ID;
|
||||
const filtered = data.filter((item) => {
|
||||
const nodeId = String(item?.lyrSn ?? item?.id ?? "");
|
||||
const upperId = String(item?.upperLyrSn ?? item?.upId ?? "");
|
||||
const layerName = String(item?.lyrNm ?? item?.name ?? "").trim();
|
||||
const layerTypeName = String(
|
||||
item?.lyrTyNm ?? item?.lyrTypeNm ?? item?.lyr_type_nm ?? "",
|
||||
).trim();
|
||||
|
||||
if (nodeId === String(existingRootId)) return false;
|
||||
if (upperId === String(existingRootId)) return false;
|
||||
if (!existingRoot && nodeId === SINKHOLE_CONTROL_NODE_ID) return false;
|
||||
if (!existingRoot && upperId === SINKHOLE_CONTROL_NODE_ID) return false;
|
||||
if (!existingRoot && layerName === SINKHOLE_LAYER_NAME) return false;
|
||||
if (!existingRoot && layerTypeName === SINKHOLE_LAYER_TYPE) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const rootNode = existingRoot
|
||||
? {
|
||||
...existingRoot,
|
||||
lyrSn: existingRootId,
|
||||
id: existingRootId,
|
||||
gobjTy: "",
|
||||
iconNm: existingRoot.iconNm || SINKHOLE_TREE_ICON_NAME,
|
||||
clickPsblYn: "N",
|
||||
actlLyrYn: "N",
|
||||
children: null,
|
||||
}
|
||||
: {
|
||||
lyrSn: SINKHOLE_CONTROL_NODE_ID,
|
||||
id: SINKHOLE_CONTROL_NODE_ID,
|
||||
lyrNm: SINKHOLE_LAYER_NAME,
|
||||
upperLyrSn: null,
|
||||
upId: null,
|
||||
sortOrdr: "3",
|
||||
drawOrdr: null,
|
||||
flterSql: null,
|
||||
gobjTy: "",
|
||||
lyrTyNm: SINKHOLE_LAYER_TYPE,
|
||||
useYn: "Y",
|
||||
minViewSize: null,
|
||||
iconNm: SINKHOLE_TREE_ICON_NAME,
|
||||
iconSize: null,
|
||||
lblAtrbNm: null,
|
||||
fontNm: null,
|
||||
fontIntnColor: null,
|
||||
fontExtnColor: null,
|
||||
fontExtnWdth: null,
|
||||
lnWdth: null,
|
||||
lnColor: null,
|
||||
polgnColor: null,
|
||||
initEprssYn: "Y",
|
||||
clickPsblYn: "N",
|
||||
exprsYn: "Y",
|
||||
cmncNm: "all",
|
||||
srvcNm: "api",
|
||||
actlLyrYn: "N",
|
||||
clckZoom: null,
|
||||
children: null,
|
||||
};
|
||||
|
||||
return [...filtered, rootNode];
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
import type { ExmnMapEntryContext } from "../components/exmn/exmn-map-types";
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getExmnMapLayer } from "../components/exmn/exmn-map-api";
|
||||
import {
|
||||
EMPTY_EXMN_MAP_FILTERS,
|
||||
EXMN_RESULT_MNHL_LAYER_ID,
|
||||
EXMN_RESULT_PPL_LAYER_ID,
|
||||
type ExmnMapFilters,
|
||||
type ExmnMapJobType,
|
||||
} from "../components/exmn/exmn-map-types";
|
||||
import {
|
||||
createExmnPipeStyle,
|
||||
createExmnManholeStyle,
|
||||
} from "./analysis-layer-styles";
|
||||
|
||||
/** 관로·맨홀 조사 결과 도형 조회와 지도 레이어 수명을 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
isExmnPplChecked: boolean;
|
||||
isExmnMnhlChecked: boolean;
|
||||
mapReady: boolean;
|
||||
exmnMapRegionCodes: { ctpvCd: string; sggCd: string };
|
||||
exmnMapEntryContext: ExmnMapEntryContext;
|
||||
isExmnPplSettingEnabled: boolean;
|
||||
exmnPplFilters: ExmnMapFilters;
|
||||
isExmnMnhlSettingEnabled: boolean;
|
||||
exmnMnhlFilters: ExmnMapFilters;
|
||||
};
|
||||
|
||||
/** 관로·맨홀 조사 결과 도형 조회와 지도 레이어 수명을 소유한다. */
|
||||
export function useExmnResultLayers({
|
||||
gisMaps,
|
||||
isExmnPplChecked,
|
||||
isExmnMnhlChecked,
|
||||
mapReady,
|
||||
exmnMapRegionCodes,
|
||||
exmnMapEntryContext,
|
||||
isExmnPplSettingEnabled,
|
||||
exmnPplFilters,
|
||||
isExmnMnhlSettingEnabled,
|
||||
exmnMnhlFilters,
|
||||
}: Options) {
|
||||
const exmnPplLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const exmnPplSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const exmnMnhlLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const exmnMnhlSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const exmnPplRequestSeqRef = useRef(0);
|
||||
|
||||
const exmnMnhlRequestSeqRef = useRef(0);
|
||||
|
||||
// 조사관리에서 지도보기로 최초 진입했을 때만 조회된 대표 시설 한 건으로 지도를 맞춘다.
|
||||
const exmnInitialFeatureFitAppliedRef = useRef(false);
|
||||
|
||||
const removeExmnResultLayer = useCallback((jobType: ExmnMapJobType) => {
|
||||
const layerId =
|
||||
jobType === "PPL" ? EXMN_RESULT_PPL_LAYER_ID : EXMN_RESULT_MNHL_LAYER_ID;
|
||||
const layerRef = jobType === "PPL" ? exmnPplLayerRef : exmnMnhlLayerRef;
|
||||
const sourceRef = jobType === "PPL" ? exmnPplSourceRef : exmnMnhlSourceRef;
|
||||
const requestSeqRef =
|
||||
jobType === "PPL" ? exmnPplRequestSeqRef : exmnMnhlRequestSeqRef;
|
||||
|
||||
requestSeqRef.current += 1;
|
||||
if (layerRef.current) {
|
||||
(GisApp.Module.select as any)?.removeLayer?.(layerRef.current);
|
||||
}
|
||||
(GisApp.Module.core as any)?.deleteLayer?.(layerId);
|
||||
layerRef.current = null;
|
||||
sourceRef.current = null;
|
||||
}, []);
|
||||
|
||||
const ensureExmnResultLayer = useCallback(
|
||||
(jobType: ExmnMapJobType) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
const layerRef = jobType === "PPL" ? exmnPplLayerRef : exmnMnhlLayerRef;
|
||||
const sourceRef =
|
||||
jobType === "PPL" ? exmnPplSourceRef : exmnMnhlSourceRef;
|
||||
if (layerRef.current && sourceRef.current) return sourceRef.current;
|
||||
|
||||
const layerId =
|
||||
jobType === "PPL"
|
||||
? EXMN_RESULT_PPL_LAYER_ID
|
||||
: EXMN_RESULT_MNHL_LAYER_ID;
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: jobType === "PPL" ? 9988 : 9989,
|
||||
style: jobType === "PPL" ? createExmnPipeStyle : createExmnManholeStyle,
|
||||
updateWhileAnimating: false,
|
||||
updateWhileInteracting: false,
|
||||
});
|
||||
layer.set("id", layerId);
|
||||
layer.set("lyrNm", jobType === "PPL" ? "관로조사" : "맨홀조사");
|
||||
layer.set(
|
||||
"lyrTyNm",
|
||||
jobType === "PPL" ? "tb_ms_swage_ppl" : "tb_ms_swage_mhl",
|
||||
);
|
||||
layer.set("clickPsblYn", "Y");
|
||||
layer.set("chcYn", "Y");
|
||||
|
||||
(GisApp.Module.core as any)?.deleteLayer?.(layerId);
|
||||
map.addLayer(layer);
|
||||
(GisApp.Module.select as any)?.addLayer?.(layer);
|
||||
layerRef.current = layer;
|
||||
sourceRef.current = source;
|
||||
return source;
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
|
||||
const loadExmnResultLayer = useCallback(
|
||||
async (jobType: ExmnMapJobType, filters: ExmnMapFilters) => {
|
||||
const isChecked =
|
||||
jobType === "PPL" ? isExmnPplChecked : isExmnMnhlChecked;
|
||||
if (!mapReady || !gisMaps.current || !isChecked) return;
|
||||
|
||||
const source = ensureExmnResultLayer(jobType);
|
||||
if (!source) return;
|
||||
const sggCodePrefix = exmnMapRegionCodes.sggCd
|
||||
.replace(/[^0-9]/g, "")
|
||||
.slice(0, 5);
|
||||
// 공통 행정구역 코드가 준비되기 전에는 전국 중복 관로를 조회하지 않고 다음 상태 갱신을 기다린다.
|
||||
if (sggCodePrefix.length !== 5) {
|
||||
source.clear();
|
||||
return;
|
||||
}
|
||||
const requestSeqRef =
|
||||
jobType === "PPL" ? exmnPplRequestSeqRef : exmnMnhlRequestSeqRef;
|
||||
const requestSeq = requestSeqRef.current + 1;
|
||||
requestSeqRef.current = requestSeq;
|
||||
|
||||
try {
|
||||
const response = await getExmnMapLayer(jobType, filters, {
|
||||
sggCd: sggCodePrefix,
|
||||
ctpvNm: exmnMapEntryContext.ctpvNm,
|
||||
sggNm: exmnMapEntryContext.sggNm,
|
||||
});
|
||||
if (requestSeqRef.current !== requestSeq) return;
|
||||
const featureCollection =
|
||||
response?.data?.type === "FeatureCollection"
|
||||
? response.data
|
||||
: response;
|
||||
const features = new GeoJSON().readFeatures(featureCollection, {
|
||||
dataProjection: "EPSG:5179",
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
source.clear();
|
||||
source.addFeatures(features);
|
||||
|
||||
// 표본 데이터가 적은 현재 단계에서는 최초 조사유형의 첫 시설을 기준으로 한 번만 확대한다.
|
||||
if (
|
||||
!exmnInitialFeatureFitAppliedRef.current &&
|
||||
exmnMapEntryContext.jobType === jobType &&
|
||||
features.length > 0
|
||||
) {
|
||||
const geometry = features[0]?.getGeometry?.();
|
||||
const extent = geometry?.getExtent?.();
|
||||
if (
|
||||
extent &&
|
||||
extent.length === 4 &&
|
||||
extent.every((coordinate: number) => Number.isFinite(coordinate))
|
||||
) {
|
||||
gisMaps.current?.getView?.().fit?.(extent, {
|
||||
duration: 300,
|
||||
padding: [80, 420, 80, 80],
|
||||
maxZoom: 18,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
exmnInitialFeatureFitAppliedRef.current = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSeqRef.current !== requestSeq) return;
|
||||
source.clear();
|
||||
console.error(
|
||||
`${jobType === "PPL" ? "관로조사" : "맨홀조사"} 레이어 조회 실패:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
ensureExmnResultLayer,
|
||||
exmnMapEntryContext.ctpvNm,
|
||||
exmnMapEntryContext.jobType,
|
||||
exmnMapEntryContext.sggNm,
|
||||
exmnMapRegionCodes.sggCd,
|
||||
gisMaps,
|
||||
isExmnMnhlChecked,
|
||||
isExmnPplChecked,
|
||||
mapReady,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !isExmnPplChecked) {
|
||||
removeExmnResultLayer("PPL");
|
||||
return;
|
||||
}
|
||||
void loadExmnResultLayer(
|
||||
"PPL",
|
||||
isExmnPplSettingEnabled ? exmnPplFilters : EMPTY_EXMN_MAP_FILTERS,
|
||||
);
|
||||
}, [
|
||||
exmnPplFilters,
|
||||
isExmnPplChecked,
|
||||
isExmnPplSettingEnabled,
|
||||
loadExmnResultLayer,
|
||||
mapReady,
|
||||
removeExmnResultLayer,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !isExmnMnhlChecked) {
|
||||
removeExmnResultLayer("MNHL");
|
||||
return;
|
||||
}
|
||||
void loadExmnResultLayer(
|
||||
"MNHL",
|
||||
isExmnMnhlSettingEnabled ? exmnMnhlFilters : EMPTY_EXMN_MAP_FILTERS,
|
||||
);
|
||||
}, [
|
||||
exmnMnhlFilters,
|
||||
isExmnMnhlChecked,
|
||||
isExmnMnhlSettingEnabled,
|
||||
loadExmnResultLayer,
|
||||
mapReady,
|
||||
removeExmnResultLayer,
|
||||
]);
|
||||
// 화면 이탈 시 두 요청을 무효화하고 자신이 등록한 도형 레이어만 제거한다.
|
||||
useEffect(
|
||||
() => () => {
|
||||
exmnPplRequestSeqRef.current += 1;
|
||||
exmnMnhlRequestSeqRef.current += 1;
|
||||
[exmnPplLayerRef, exmnMnhlLayerRef].forEach((ref) => {
|
||||
if (ref.current) {
|
||||
(GisApp.Module.select as any)?.removeLayer?.(ref.current);
|
||||
gisMaps.current?.removeLayer?.(ref.current);
|
||||
}
|
||||
ref.current = null;
|
||||
});
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
return { removeExmnResultLayer };
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import type VectorLayer from "ol/layer/Vector.js";
|
||||
import type VectorSource from "ol/source/Vector.js";
|
||||
import type Geometry from "ol/geom/Geometry.js";
|
||||
import { useEffect } from "react";
|
||||
import { unByKey } from "ol/Observable.js";
|
||||
import { type FldngHstryItem } from "@/entities/flood-history/model/flood-history-types";
|
||||
|
||||
/** 침수 이력 강조 도형 클릭과 상세 카드 위치를 연결하고 이벤트를 해제한다. */
|
||||
type Options = {
|
||||
mapReady: boolean;
|
||||
gisMaps: React.RefObject<any>;
|
||||
selectedFldngHstryDetail: FldngHstryItem | null;
|
||||
isFldngHstryChecked: boolean;
|
||||
fldngHstryAreaLayerRef: React.RefObject<VectorLayer<
|
||||
VectorSource<Geometry>
|
||||
> | null>;
|
||||
fldngHstryDepthLayerRef: React.RefObject<VectorLayer<
|
||||
VectorSource<Geometry>
|
||||
> | null>;
|
||||
setFldngHstryDetailPopupPosition: React.Dispatch<
|
||||
React.SetStateAction<{ left: number; top: number } | null>
|
||||
>;
|
||||
setIsSearchModalOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
/** 침수 이력 강조 도형 클릭과 상세 카드 위치를 연결하고 이벤트를 해제한다. */
|
||||
export function useFloodHistoryClick({
|
||||
mapReady,
|
||||
gisMaps,
|
||||
selectedFldngHstryDetail,
|
||||
isFldngHstryChecked,
|
||||
fldngHstryAreaLayerRef,
|
||||
fldngHstryDepthLayerRef,
|
||||
setFldngHstryDetailPopupPosition,
|
||||
setIsSearchModalOpen,
|
||||
}: Options) {
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const clickKey = map.on("singleclick", (event: any) => {
|
||||
if (!selectedFldngHstryDetail || !isFldngHstryChecked) return;
|
||||
|
||||
let clickedFldngHstry = false;
|
||||
map.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
(feature: any, layer: any) => {
|
||||
if (
|
||||
layer === fldngHstryAreaLayerRef.current ||
|
||||
layer === fldngHstryDepthLayerRef.current
|
||||
) {
|
||||
clickedFldngHstry = true;
|
||||
return feature;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
{ hitTolerance: 4 },
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
setFldngHstryDetailPopupPosition({
|
||||
left: nextLeft,
|
||||
top: nextTop,
|
||||
});
|
||||
setIsSearchModalOpen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unByKey(clickKey);
|
||||
};
|
||||
}, [
|
||||
gisMaps,
|
||||
fldngHstryAreaLayerRef,
|
||||
fldngHstryDepthLayerRef,
|
||||
setFldngHstryDetailPopupPosition,
|
||||
isFldngHstryChecked,
|
||||
mapReady,
|
||||
selectedFldngHstryDetail,
|
||||
setIsSearchModalOpen,
|
||||
]);
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
import Polygon from "ol/geom/Polygon.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import {
|
||||
FLDNG_HSTRY_AREA_LAYER_ID,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_ID,
|
||||
FLDNG_HSTRY_PREVIEW_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_NAME,
|
||||
type FldngHstryItem,
|
||||
} from "@/entities/flood-history/model/flood-history-types";
|
||||
import { FLDNG_HSTRY_SELECTION_PADDING } from "../model/map-constants";
|
||||
import {
|
||||
createFldngHstryAreaStyle,
|
||||
createFldngHstryDepthStyle,
|
||||
} from "./analysis-layer-styles";
|
||||
|
||||
/** 침수 이력 선택 도형과 침수심 강조 레이어를 소유한다. */
|
||||
type Options = {
|
||||
mapReady: boolean;
|
||||
gisMaps: React.RefObject<any>;
|
||||
selectedFldngHstryDetail: FldngHstryItem | null;
|
||||
};
|
||||
|
||||
/** 침수 이력 선택 도형과 침수심 강조 레이어를 소유한다. */
|
||||
export function useFloodHistorySelection({
|
||||
mapReady,
|
||||
gisMaps,
|
||||
selectedFldngHstryDetail,
|
||||
}: Options) {
|
||||
// 침수발생이력 overlay layer는 지도 드래그 중에도 유지돼야 하므로 React state 대신 ref로 보관한다.
|
||||
const fldngHstryAreaLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
|
||||
|
||||
const fldngHstryDepthLayerRef = useRef<VectorLayer<VectorSource> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const ensureFldngHstryAreaSource = useCallback(() => {
|
||||
if (!mapReady || !gisMaps.current) return null;
|
||||
|
||||
if (!fldngHstryAreaLayerRef.current) {
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9993,
|
||||
});
|
||||
layer.set("id", FLDNG_HSTRY_AREA_LAYER_ID);
|
||||
layer.set("lyrNm", FLDNG_HSTRY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLDNG_HSTRY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
fldngHstryAreaLayerRef.current = layer;
|
||||
}
|
||||
|
||||
return fldngHstryAreaLayerRef.current.getSource();
|
||||
}, [gisMaps, mapReady]);
|
||||
|
||||
const ensureFldngHstryDepthSource = useCallback(() => {
|
||||
if (!mapReady || !gisMaps.current) return null;
|
||||
|
||||
if (!fldngHstryDepthLayerRef.current) {
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9992,
|
||||
});
|
||||
layer.set("id", FLDNG_HSTRY_DEPTH_LAYER_ID);
|
||||
layer.set("lyrNm", FLDNG_HSTRY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLDNG_HSTRY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
fldngHstryDepthLayerRef.current = layer;
|
||||
}
|
||||
|
||||
return fldngHstryDepthLayerRef.current.getSource();
|
||||
}, [gisMaps, mapReady]);
|
||||
|
||||
const clearFldngHstryLayers = useCallback(() => {
|
||||
fldngHstryAreaLayerRef.current?.getSource?.()?.clear?.();
|
||||
fldngHstryDepthLayerRef.current?.getSource?.()?.clear?.();
|
||||
}, []);
|
||||
|
||||
const renderSelectedFldngHstry = useCallback(
|
||||
(detail: FldngHstryItem | null) => {
|
||||
const areaSource = ensureFldngHstryAreaSource();
|
||||
const depthSource = ensureFldngHstryDepthSource();
|
||||
if (!areaSource || !depthSource) return;
|
||||
|
||||
areaSource.clear();
|
||||
depthSource.clear();
|
||||
|
||||
if (!detail) return;
|
||||
|
||||
const areaFeature = new Feature({
|
||||
geometry: new Polygon([detail.areaRing]),
|
||||
});
|
||||
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(
|
||||
createFldngHstryDepthStyle(zone.depthLabel, zone.color),
|
||||
);
|
||||
depthSource.addFeature(zoneFeature);
|
||||
});
|
||||
},
|
||||
[ensureFldngHstryAreaSource, ensureFldngHstryDepthSource],
|
||||
);
|
||||
|
||||
const focusFldngHstryOnMap = useCallback(
|
||||
(detail: FldngHstryItem) => {
|
||||
if (!gisMaps.current) return;
|
||||
|
||||
const geometry = new Polygon([detail.areaRing]);
|
||||
gisMaps.current.getView?.().fit?.(geometry.getExtent(), {
|
||||
duration: 350,
|
||||
padding: FLDNG_HSTRY_SELECTION_PADDING,
|
||||
maxZoom: 15,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
renderSelectedFldngHstry(selectedFldngHstryDetail);
|
||||
}, [renderSelectedFldngHstry, selectedFldngHstryDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearFldngHstryLayers();
|
||||
};
|
||||
}, [clearFldngHstryLayers]);
|
||||
return {
|
||||
fldngHstryAreaLayerRef,
|
||||
fldngHstryDepthLayerRef,
|
||||
clearFldngHstryLayers,
|
||||
renderSelectedFldngHstry,
|
||||
focusFldngHstryOnMap,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import {
|
||||
FLDNG_HSTRY_DEPTH_LAYER_NAME,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_NAME,
|
||||
type FldngHstryYearRange,
|
||||
} from "@/entities/flood-history/model/flood-history-types";
|
||||
import { buildFldngHstryYearCql } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
import {
|
||||
FLDNG_HSTRY_WFS_LAYER_ID,
|
||||
FLDNG_DEPTH_WFS_LAYER_ID_PREFIX,
|
||||
} from "../model/map-constants";
|
||||
|
||||
/** 침수 이력 WFS 레이어의 연도 조건과 표시 상태를 동기화한다. */
|
||||
type Options = {
|
||||
mapReady: boolean;
|
||||
gisMaps: React.RefObject<any>;
|
||||
fldngHstryYearRange: FldngHstryYearRange;
|
||||
isFldngHstryYearFilterEnabled: boolean;
|
||||
clearFldngHstrySelection: () => void;
|
||||
isFldngHstryWtlvChecked: boolean;
|
||||
isFldngHstryDepthChecked: boolean;
|
||||
checkedFldngHstryDepthLayerKeys: string[];
|
||||
isFldngHstryChecked: boolean;
|
||||
};
|
||||
|
||||
/** 침수 이력 WFS 레이어의 연도 조건과 표시 상태를 동기화한다. */
|
||||
export function useFloodHistoryVisibility({
|
||||
mapReady,
|
||||
gisMaps,
|
||||
fldngHstryYearRange,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
clearFldngHstrySelection,
|
||||
isFldngHstryWtlvChecked,
|
||||
isFldngHstryDepthChecked,
|
||||
checkedFldngHstryDepthLayerKeys,
|
||||
isFldngHstryChecked,
|
||||
}: Options) {
|
||||
// 체크 등급 변경과 연도 필터 변경을 구분해 연도 변경 때만 기존 선택 상세를 정리한다.
|
||||
const fldngHstryYearFilterSignatureRef = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
const yearCql = buildFldngHstryYearCql(
|
||||
fldngHstryYearRange,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
);
|
||||
const yearFilterSignature = `${fldngHstryYearRange.startYear}:${fldngHstryYearRange.endYear}:${isFldngHstryYearFilterEnabled}`;
|
||||
if (
|
||||
fldngHstryYearFilterSignatureRef.current &&
|
||||
fldngHstryYearFilterSignatureRef.current !== yearFilterSignature
|
||||
) {
|
||||
clearFldngHstrySelection();
|
||||
}
|
||||
fldngHstryYearFilterSignatureRef.current = yearFilterSignature;
|
||||
|
||||
/** 기준 레이어 필터를 보존하면서 선택한 침수년도 조건을 결합한다. */
|
||||
const resolveFldngHstryFilter = (baseLayerCode: any) => {
|
||||
const baseFilter = String(baseLayerCode?.flterSql ?? "").trim();
|
||||
return [baseFilter && `(${baseFilter})`, yearCql]
|
||||
.filter(Boolean)
|
||||
.join(" AND ");
|
||||
};
|
||||
|
||||
type FldngHstryLayerSpec = {
|
||||
id: string;
|
||||
layerType: string;
|
||||
layerName: string;
|
||||
baseLayerCode: any;
|
||||
filterSql: string;
|
||||
};
|
||||
const desiredLayers: FldngHstryLayerSpec[] = [];
|
||||
const layerCodes = Object.values(GisApp.layerCode ?? {}) as any[];
|
||||
|
||||
if (isFldngHstryWtlvChecked) {
|
||||
const baseLayerCode = layerCodes.find(
|
||||
(item) =>
|
||||
item?.lyrTyNm === FLDNG_HSTRY_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
);
|
||||
if (baseLayerCode) {
|
||||
desiredLayers.push({
|
||||
id: FLDNG_HSTRY_WFS_LAYER_ID,
|
||||
layerType: FLDNG_HSTRY_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLDNG_HSTRY_LAYER_NAME,
|
||||
baseLayerCode,
|
||||
filterSql: resolveFldngHstryFilter(baseLayerCode),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isFldngHstryDepthChecked) {
|
||||
const selectedDepthLayerKeys = new Set(checkedFldngHstryDepthLayerKeys);
|
||||
layerCodes
|
||||
.filter((item) => {
|
||||
const layerKey = String(item?.lyrSn ?? item?.id ?? "").trim();
|
||||
return (
|
||||
item?.lyrTyNm === FLDNG_HSTRY_DEPTH_LAYER_TYPE &&
|
||||
item?.actlLyrYn === "Y" &&
|
||||
selectedDepthLayerKeys.has(layerKey)
|
||||
);
|
||||
})
|
||||
.forEach((baseLayerCode) => {
|
||||
const layerKey = String(
|
||||
baseLayerCode?.lyrSn ?? baseLayerCode?.id,
|
||||
).trim();
|
||||
desiredLayers.push({
|
||||
id: `${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_${layerKey}`,
|
||||
layerType: FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLDNG_HSTRY_DEPTH_LAYER_NAME,
|
||||
baseLayerCode,
|
||||
filterSql: resolveFldngHstryFilter(baseLayerCode),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const desiredLayerIds = new Set(desiredLayers.map((item) => item.id));
|
||||
const mapLayers = [...(gisMaps.current?.getLayers?.()?.getArray?.() ?? [])];
|
||||
const isManagedFldngHstryLayerId = (id: string) =>
|
||||
id === FLDNG_HSTRY_WFS_LAYER_ID ||
|
||||
id.startsWith(`${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_`);
|
||||
|
||||
// 체크 해제된 등급만 삭제하고, 계속 체크된 등급은 지도에 그대로 유지한다.
|
||||
mapLayers.forEach((mapLayer: any) => {
|
||||
const layerId = String(mapLayer?.get?.("id") ?? "");
|
||||
if (
|
||||
isManagedFldngHstryLayerId(layerId) &&
|
||||
!desiredLayerIds.has(layerId)
|
||||
) {
|
||||
gisMaps.current?.removeLayer?.(mapLayer);
|
||||
}
|
||||
});
|
||||
|
||||
desiredLayers.forEach((spec) => {
|
||||
const filterSignature = `${spec.layerType}|${spec.filterSql}`;
|
||||
// 공통 getLayerById는 ID를 Number로 비교하므로 문자열 ID인 필터 레이어를 찾지 못한다.
|
||||
// 지도 컬렉션에서 문자열 ID를 직접 비교해 기존 레이어와 WFS source를 재사용한다.
|
||||
const existingLayer = gisMaps.current
|
||||
?.getLayers?.()
|
||||
?.getArray?.()
|
||||
?.find(
|
||||
(mapLayer: any) => String(mapLayer?.get?.("id") ?? "") === spec.id,
|
||||
);
|
||||
if (
|
||||
existingLayer?.get?.("fldngHstryFilterSignature") === filterSignature
|
||||
) {
|
||||
existingLayer?.setVisible?.(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 연도나 적용 여부가 달라진 레이어만 교체하고, 동일 필터 레이어는 재사용한다.
|
||||
core?.deleteLayer?.(spec.id);
|
||||
const layerCode = {
|
||||
...spec.baseLayerCode,
|
||||
id: spec.id,
|
||||
lyrSn: spec.id,
|
||||
lyrNm: spec.layerName,
|
||||
lyrTyNm: spec.layerType,
|
||||
actlLyrYn: "Y",
|
||||
exprsYn: "Y",
|
||||
clickPsblYn: "Y",
|
||||
cmncNm: "bbox",
|
||||
srvcNm: "wfs",
|
||||
flterSql: spec.filterSql,
|
||||
};
|
||||
const layer = core?.addWfsLayer?.(layerCode);
|
||||
layer?.set?.("id", spec.id);
|
||||
layer?.set?.("lyrNm", spec.layerName);
|
||||
layer?.set?.("lyrTyNm", spec.layerType);
|
||||
layer?.set?.("fldngHstryFilterSignature", filterSignature);
|
||||
});
|
||||
|
||||
// 전용 필터 레이어가 담당하는 유형의 원본 레이어만 숨겨 중복 표출을 막는다.
|
||||
const activeFilteredTypes = new Set(
|
||||
desiredLayers.map((item) => item.layerType),
|
||||
);
|
||||
gisMaps.current
|
||||
?.getLayers?.()
|
||||
?.getArray?.()
|
||||
?.forEach((mapLayer: any) => {
|
||||
const layerId = String(mapLayer?.get?.("id") ?? "");
|
||||
const layerType = String(mapLayer?.get?.("lyrTyNm") ?? "");
|
||||
if (
|
||||
activeFilteredTypes.has(layerType) &&
|
||||
!isManagedFldngHstryLayerId(layerId)
|
||||
) {
|
||||
mapLayer?.setVisible?.(false);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
checkedFldngHstryDepthLayerKeys,
|
||||
clearFldngHstrySelection,
|
||||
fldngHstryYearRange,
|
||||
gisMaps,
|
||||
isFldngHstryChecked,
|
||||
isFldngHstryDepthChecked,
|
||||
isFldngHstryWtlvChecked,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
mapReady,
|
||||
]);
|
||||
|
||||
// 페이지가 닫힐 때만 관리 대상 필터 레이어 전체를 정리한다.
|
||||
useEffect(() => {
|
||||
if (!mapReady) return;
|
||||
const map = gisMaps.current;
|
||||
return () => {
|
||||
const layers = [...(map?.getLayers?.()?.getArray?.() ?? [])];
|
||||
layers.forEach((layer: any) => {
|
||||
const layerId = String(layer?.get?.("id") ?? "");
|
||||
if (
|
||||
layerId === FLDNG_HSTRY_WFS_LAYER_ID ||
|
||||
layerId.startsWith(`${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_`)
|
||||
) {
|
||||
map?.removeLayer?.(layer);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [gisMaps, mapReady]);
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { useCallback, useRef, useEffect } from "react";
|
||||
import Overlay from "ol/Overlay.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { getFloodRiskGradeClassName } from "@/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content";
|
||||
import { FLOOD_PREDICTION_GRID_LAYER_ID } from "../model/map-constants";
|
||||
import { formatFloodPredictionDateTime } from "./map-values";
|
||||
|
||||
/** 침수예측 선택행의 격자 중앙 정보 오버레이를 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
};
|
||||
|
||||
/** 침수예측 선택행의 격자 중앙 정보 오버레이를 소유한다. */
|
||||
export function useFloodPredictionGridOverlay({ gisMaps }: Options) {
|
||||
// 침수예측현황 선택행에 따라 초단기예보 격자 중앙 라벨을 교체하는 전용 overlay를 보관한다.
|
||||
const floodPredictionGridLayerRef = useRef<Overlay | null>(null);
|
||||
const requestSequence = useRef(0);
|
||||
|
||||
/** 기존 침수예측 중앙 오버레이를 제거한다. */
|
||||
const removeFloodPredictionGridLayer = useCallback(() => {
|
||||
requestSequence.current += 1;
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(FLOOD_PREDICTION_GRID_LAYER_ID);
|
||||
const map = gisMaps.current;
|
||||
if (map && floodPredictionGridLayerRef.current) {
|
||||
map.removeOverlay(floodPredictionGridLayerRef.current);
|
||||
}
|
||||
floodPredictionGridLayerRef.current = null;
|
||||
}, [gisMaps]);
|
||||
|
||||
/** 선택행의 시군구·분석시각으로 도시침수지도 중앙 오버레이를 표시한다. */
|
||||
const loadFloodPredictionGridLayer = useCallback(
|
||||
async (sggCd: string, analysisDateTime: string, extent: number[]) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map || !/^\d{5}$/.test(sggCd) || !/^\d{12}$/.test(analysisDateTime))
|
||||
return;
|
||||
removeFloodPredictionGridLayer();
|
||||
const sequence = requestSequence.current;
|
||||
try {
|
||||
const response = await getRawJson(
|
||||
`/sewerage-service/api/v1/fldng/predc/prst/gridMap?sggCd=${encodeURIComponent(sggCd)}&analysisDateTime=${encodeURIComponent(analysisDateTime)}`,
|
||||
);
|
||||
if (sequence !== requestSequence.current) return;
|
||||
const payload = response?.data ?? response;
|
||||
const row = payload && !Array.isArray(payload) ? payload : null;
|
||||
if (
|
||||
!row ||
|
||||
extent.length !== 4 ||
|
||||
extent.some((value) => !Number.isFinite(value))
|
||||
)
|
||||
return;
|
||||
const center = [
|
||||
(extent[0]! + extent[2]!) / 2,
|
||||
(extent[1]! + extent[3]!) / 2,
|
||||
];
|
||||
const observed = Number(row.observedAccumulatedRainfall ?? 0).toFixed(
|
||||
1,
|
||||
);
|
||||
const predicted = Number(row.predictedAccumulatedRainfall ?? 0).toFixed(
|
||||
1,
|
||||
);
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText =
|
||||
"position:relative; background:rgba(255,255,255,.55); color:#111827; border:1px solid rgba(17,24,39,.7); box-shadow:0 2px 8px rgba(0,0,0,.2); padding:8px 10px; font:400 11px/1.35 sans-serif; min-width:230px;";
|
||||
const close = document.createElement("button");
|
||||
close.type = "button";
|
||||
close.textContent = "×";
|
||||
close.title = "침수예측 오버레이 닫기";
|
||||
close.style.cssText =
|
||||
"position:absolute; right:3px; top:1px; border:0; background:transparent; color:#111827; font-size:16px; line-height:16px; cursor:pointer;";
|
||||
close.addEventListener("click", () => removeFloodPredictionGridLayer());
|
||||
const table = document.createElement("table");
|
||||
table.style.cssText = "border-collapse:collapse; width:100%;";
|
||||
const title = table.createTHead().insertRow();
|
||||
const titleCell = title.insertCell();
|
||||
titleCell.colSpan = 2;
|
||||
titleCell.textContent = `${row.ctpvNm ?? row.ctpvCd ?? "-"} ${row.sggNm ?? row.sggCd ?? "-"} 침수예측`;
|
||||
titleCell.style.cssText =
|
||||
"font-weight:500; text-align:center; padding:2px 16px 6px 2px; border-bottom:1px solid #9ca3af;";
|
||||
const body = table.createTBody();
|
||||
[
|
||||
[
|
||||
"분석시각",
|
||||
formatFloodPredictionDateTime(
|
||||
row.analysisDateTime ?? analysisDateTime,
|
||||
),
|
||||
],
|
||||
[
|
||||
"강우시작시각",
|
||||
formatFloodPredictionDateTime(row.rainBeginningDateTime),
|
||||
],
|
||||
["관측 누적강수량(mm)", observed],
|
||||
["예측시각", formatFloodPredictionDateTime(row.predictionDateTime)],
|
||||
["예측 누적강수량(mm)", predicted],
|
||||
["초과 빈도", `${row.exceededFrequencyYear ?? "-"}년`],
|
||||
["침수위험도", row.floodRiskGrade ?? "-"],
|
||||
].forEach(([label, value]) => {
|
||||
const tr = body.insertRow();
|
||||
const key = tr.insertCell();
|
||||
key.textContent = label;
|
||||
key.style.cssText =
|
||||
"font-weight:400; padding:2px 8px 2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;";
|
||||
const val = tr.insertCell();
|
||||
val.style.cssText =
|
||||
"font-weight:400; text-align:right; padding:2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;";
|
||||
if (label === "침수위험도") {
|
||||
// 침수예측현황 목록과 같은 배지 스타일로 등급을 표시한다.
|
||||
const badge = document.createElement("span");
|
||||
badge.textContent = value;
|
||||
badge.className = `sn-popup-badge min-w-[44px] ${getFloodRiskGradeClassName(String(value ?? ""))}`;
|
||||
val.append(badge);
|
||||
} else {
|
||||
val.textContent = value;
|
||||
}
|
||||
});
|
||||
container.append(close, table);
|
||||
const overlay = new Overlay({
|
||||
element: container,
|
||||
positioning: "center-center",
|
||||
stopEvent: true,
|
||||
});
|
||||
overlay.set("id", FLOOD_PREDICTION_GRID_LAYER_ID);
|
||||
overlay.setPosition(center);
|
||||
map.addOverlay(overlay);
|
||||
floodPredictionGridLayerRef.current = overlay;
|
||||
} catch (error) {
|
||||
if (sequence !== requestSequence.current) return;
|
||||
console.error("침수예측지도 오버레이 표시 실패:", error);
|
||||
}
|
||||
},
|
||||
[gisMaps, removeFloodPredictionGridLayer],
|
||||
);
|
||||
useEffect(
|
||||
() => () => {
|
||||
requestSequence.current += 1;
|
||||
if (floodPredictionGridLayerRef.current)
|
||||
gisMaps.current?.removeOverlay?.(floodPredictionGridLayerRef.current);
|
||||
floodPredictionGridLayerRef.current = null;
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
return { loadFloodPredictionGridLayer };
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
"use client";
|
||||
import type Geometry from "ol/geom/Geometry.js";
|
||||
import type * as React from "react";
|
||||
import { createGisEditPreviewStyle } from "@/features/gis-edit/lib/preview-styles";
|
||||
import { applyGeometryVertexSnaps } from "@/features/gis-edit/lib/geometry-mutations";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import Draw from "ol/interaction/Draw.js";
|
||||
import Snap from "ol/interaction/Snap.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import { FLOW_DIRECTION_SRS } from "../model/map-constants";
|
||||
|
||||
/** 신규 도형 Draw·Snap interaction의 생성과 종료를 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
mapReady: boolean;
|
||||
removeGisEditInteractions: () => void;
|
||||
ensureGisEditPreviewLayer: () => VectorSource<Geometry> | null;
|
||||
shouldUsePipeSnapSource: boolean;
|
||||
ensureGisEditPipeSnapSource: () => VectorSource<Geometry>;
|
||||
setGisEditPreviewCursor: (cursor: string) => void;
|
||||
gisEditPreviewLayerRef: React.RefObject<any>;
|
||||
openGisEditPipeDetail: (changeId: string | null) => void;
|
||||
setFeatureData: React.Dispatch<any>;
|
||||
setGisEditDetailChangeId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsSearchModalOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
/** 신규 도형 Draw·Snap interaction의 생성과 종료를 소유한다. */
|
||||
export function useGisEditDraw({
|
||||
gisMaps,
|
||||
gisEdit,
|
||||
mapReady,
|
||||
removeGisEditInteractions,
|
||||
ensureGisEditPreviewLayer,
|
||||
shouldUsePipeSnapSource,
|
||||
ensureGisEditPipeSnapSource,
|
||||
setGisEditPreviewCursor,
|
||||
gisEditPreviewLayerRef,
|
||||
openGisEditPipeDetail,
|
||||
setFeatureData,
|
||||
setGisEditDetailChangeId,
|
||||
setIsOpen,
|
||||
setIsSearchModalOpen,
|
||||
}: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const {
|
||||
selectedLayer: editSelectedLayer,
|
||||
isEditMode: editIsEditMode,
|
||||
activeTool: editActiveTool,
|
||||
registerAddGeometry: editRegisterAddGeometry,
|
||||
deactivateTool: editDeactivateTool,
|
||||
announceStatus: editAnnounceStatus,
|
||||
isSnapEnabled: editIsSnapEnabled,
|
||||
} = gisEdit;
|
||||
|
||||
const gisEditDrawRef = useRef<any | null>(null);
|
||||
|
||||
const gisEditDrawSnapRef = useRef<any | null>(null);
|
||||
|
||||
const removeGisEditDrawInteraction = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
if (gisEditDrawSnapRef.current) {
|
||||
map.removeInteraction(gisEditDrawSnapRef.current);
|
||||
gisEditDrawSnapRef.current = null;
|
||||
}
|
||||
if (gisEditDrawRef.current) {
|
||||
map.removeInteraction(gisEditDrawRef.current);
|
||||
gisEditDrawRef.current = null;
|
||||
}
|
||||
}, [gisMaps]);
|
||||
|
||||
const gisEditSelectedLayerId = editSelectedLayer?.layerId ?? "";
|
||||
|
||||
const gisEditSelectedLayerGeometryType =
|
||||
editSelectedLayer?.geometryType ?? null;
|
||||
|
||||
const gisEditSelectedLayerPreviewColors =
|
||||
editSelectedLayer?.previewColors ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current || !editIsEditMode) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
|
||||
if (
|
||||
editActiveTool !== "ADD" ||
|
||||
!gisEditSelectedLayerId ||
|
||||
!gisEditSelectedLayerGeometryType
|
||||
) {
|
||||
removeGisEditDrawInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
removeGisEditInteractions();
|
||||
removeGisEditDrawInteraction();
|
||||
|
||||
const drawSource = ensureGisEditPreviewLayer();
|
||||
if (!drawSource) return;
|
||||
const drawType =
|
||||
gisEditSelectedLayerGeometryType === "Point"
|
||||
? "Point"
|
||||
: gisEditSelectedLayerGeometryType === "Polygon"
|
||||
? "Polygon"
|
||||
: "LineString";
|
||||
|
||||
const draw = new Draw({
|
||||
source: drawSource,
|
||||
type: drawType,
|
||||
});
|
||||
const pipeSnapSource = shouldUsePipeSnapSource
|
||||
? ensureGisEditPipeSnapSource()
|
||||
: null;
|
||||
|
||||
draw.on("drawstart", () => {
|
||||
setGisEditPreviewCursor("crosshair");
|
||||
});
|
||||
|
||||
draw.on("drawend", (event: any) => {
|
||||
try {
|
||||
const pipeSnapSource = shouldUsePipeSnapSource
|
||||
? ensureGisEditPipeSnapSource()
|
||||
: null;
|
||||
if (pipeSnapSource?.getFeatures?.().length) {
|
||||
applyGeometryVertexSnaps(
|
||||
event.feature.getGeometry(),
|
||||
pipeSnapSource,
|
||||
map,
|
||||
20,
|
||||
);
|
||||
}
|
||||
const geometryObject = new GeoJSON().writeGeometryObject(
|
||||
event.feature.getGeometry(),
|
||||
{
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
},
|
||||
) as Record<string, unknown>;
|
||||
const changeId = editRegisterAddGeometry(geometryObject);
|
||||
if (changeId) {
|
||||
event.feature.setId(`gis-edit:${changeId}`);
|
||||
event.feature.set("fid", changeId);
|
||||
event.feature.set("editType", "ADD");
|
||||
event.feature.set("objectId", changeId);
|
||||
event.feature.set("layerId", gisEditSelectedLayerId);
|
||||
event.feature.set("changeId", changeId);
|
||||
event.feature.set(
|
||||
"layerPointColor",
|
||||
gisEditSelectedLayerPreviewColors?.pointColor ?? "",
|
||||
);
|
||||
event.feature.set(
|
||||
"layerLineColor",
|
||||
gisEditSelectedLayerPreviewColors?.lineColor ?? "",
|
||||
);
|
||||
event.feature.set(
|
||||
"layerFillColor",
|
||||
gisEditSelectedLayerPreviewColors?.fillColor ?? "",
|
||||
);
|
||||
event.feature.set("isInteractionTarget", false);
|
||||
event.feature.set("isDragging", false);
|
||||
event.feature.set("previewLabel", "신규 추가");
|
||||
event.feature.setStyle(createGisEditPreviewStyle(event.feature));
|
||||
gisMaps.current?.renderSync?.();
|
||||
window.setTimeout(() => {
|
||||
const source = ensureGisEditPreviewLayer();
|
||||
if (!source) return;
|
||||
if (!source.getFeatureById?.(`gis-edit:${changeId}`)) {
|
||||
source.addFeature(event.feature);
|
||||
}
|
||||
event.feature.changed?.();
|
||||
source.changed?.();
|
||||
gisEditPreviewLayerRef.current?.changed?.();
|
||||
gisMaps.current?.renderSync?.();
|
||||
}, 0);
|
||||
} else {
|
||||
window.setTimeout(() => {
|
||||
drawSource.removeFeature(event.feature);
|
||||
}, 0);
|
||||
}
|
||||
if (changeId && gisEditSelectedLayerId === "tb_ms_swage_ppl") {
|
||||
openGisEditPipeDetail(changeId);
|
||||
setFeatureData({
|
||||
lyrTypeNm: "tb_ms_swage_ppl",
|
||||
fcltMngNo: "",
|
||||
mngInstCd: "",
|
||||
fclt_mng_no: "",
|
||||
inst_mng_no: "",
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
setGisEditDetailChangeId(changeId);
|
||||
setIsOpen(true);
|
||||
}, 0);
|
||||
setIsSearchModalOpen(false);
|
||||
window.setTimeout(() => {
|
||||
editDeactivateTool();
|
||||
removeGisEditDrawInteraction();
|
||||
}, 0);
|
||||
editAnnounceStatus(
|
||||
"하수관로 신규 객체를 지도에 추가했습니다. 우측 상세정보를 입력한 뒤 저장하세요.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("GIS 편집 ADD geometry 변환 실패:", error);
|
||||
editAnnounceStatus(
|
||||
"신규 geometry를 추가하지 못했습니다. 다시 시도해 주세요.",
|
||||
);
|
||||
} finally {
|
||||
setGisEditPreviewCursor("crosshair");
|
||||
}
|
||||
});
|
||||
|
||||
map.addInteraction(draw);
|
||||
gisEditDrawRef.current = draw;
|
||||
if (pipeSnapSource) {
|
||||
const drawSnap = new Snap({
|
||||
source: pipeSnapSource,
|
||||
edge: true,
|
||||
vertex: true,
|
||||
pixelTolerance: 14,
|
||||
});
|
||||
map.addInteraction(drawSnap);
|
||||
gisEditDrawSnapRef.current = drawSnap;
|
||||
}
|
||||
|
||||
return () => {
|
||||
removeGisEditDrawInteraction();
|
||||
};
|
||||
}, [
|
||||
ensureGisEditPreviewLayer,
|
||||
ensureGisEditPipeSnapSource,
|
||||
editActiveTool,
|
||||
editAnnounceStatus,
|
||||
editDeactivateTool,
|
||||
editIsEditMode,
|
||||
editIsSnapEnabled,
|
||||
editRegisterAddGeometry,
|
||||
gisEditSelectedLayerGeometryType,
|
||||
gisEditSelectedLayerId,
|
||||
gisEditSelectedLayerPreviewColors,
|
||||
mapReady,
|
||||
openGisEditPipeDetail,
|
||||
removeGisEditDrawInteraction,
|
||||
removeGisEditInteractions,
|
||||
setGisEditPreviewCursor,
|
||||
shouldUsePipeSnapSource,
|
||||
gisMaps,
|
||||
gisEditPreviewLayerRef,
|
||||
setFeatureData,
|
||||
setIsSearchModalOpen,
|
||||
setGisEditDetailChangeId,
|
||||
setIsOpen,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editIsEditMode || editActiveTool !== "ADD") {
|
||||
removeGisEditDrawInteraction();
|
||||
}
|
||||
}, [editActiveTool, editIsEditMode, removeGisEditDrawInteraction]);
|
||||
useEffect(
|
||||
() => () => removeGisEditDrawInteraction(),
|
||||
[removeGisEditDrawInteraction],
|
||||
);
|
||||
return { removeGisEditDrawInteraction };
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
import type { RefObject } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import {
|
||||
normalizeGisEditFid,
|
||||
getGisEditFeatureFid,
|
||||
buildGisEditHiddenPipeFilter,
|
||||
} from "./gis-edit-features";
|
||||
|
||||
/** 편집 중 원본 도형의 필터와 스타일을 보관하고 종료 시 복원한다. */
|
||||
type Options = {
|
||||
gisMaps: RefObject<any>;
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
mapReady: boolean;
|
||||
};
|
||||
|
||||
/** 편집 중 원본 도형의 필터와 스타일을 보관하고 종료 시 복원한다. */
|
||||
export function useGisEditOriginalMask({
|
||||
gisMaps,
|
||||
gisEdit,
|
||||
mapReady,
|
||||
}: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const { changes: editChanges, isEditMode: editIsEditMode } = gisEdit;
|
||||
|
||||
const gisEditOriginalLayerFilterRef = useRef<
|
||||
globalThis.Map<any, string | null>
|
||||
>(new globalThis.Map());
|
||||
|
||||
const gisEditOriginalFeatureStyleRef = useRef<globalThis.Map<any, any>>(
|
||||
new globalThis.Map(),
|
||||
);
|
||||
|
||||
const restoreGisEditOriginalPipeMask = useCallback(() => {
|
||||
gisEditOriginalLayerFilterRef.current.forEach((originalFilter, layer) => {
|
||||
const source = layer?.getSource?.();
|
||||
const params = { ...(source?.getParams?.() ?? {}) };
|
||||
if (originalFilter) {
|
||||
params.CQL_FILTER = originalFilter;
|
||||
} else {
|
||||
delete params.CQL_FILTER;
|
||||
}
|
||||
params._t = Date.now();
|
||||
source?.updateParams?.(params);
|
||||
source?.refresh?.();
|
||||
source?.changed?.();
|
||||
layer?.changed?.();
|
||||
});
|
||||
gisEditOriginalLayerFilterRef.current.clear();
|
||||
|
||||
gisEditOriginalFeatureStyleRef.current.forEach((originalStyle, feature) => {
|
||||
feature?.setStyle?.(originalStyle);
|
||||
feature?.changed?.();
|
||||
});
|
||||
gisEditOriginalFeatureStyleRef.current.clear();
|
||||
gisMaps.current?.renderSync?.();
|
||||
}, [gisMaps]);
|
||||
|
||||
const applyGisEditOriginalPipeMask = useCallback(
|
||||
(hiddenFids: string[]) => {
|
||||
const hiddenFidSet = new Set(
|
||||
hiddenFids.map((fid) => normalizeGisEditFid(fid)).filter(Boolean),
|
||||
);
|
||||
|
||||
if (hiddenFidSet.size === 0) {
|
||||
restoreGisEditOriginalPipeMask();
|
||||
return;
|
||||
}
|
||||
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
const filter = buildGisEditHiddenPipeFilter(Array.from(hiddenFidSet));
|
||||
map.getAllLayers?.().forEach((layer: any) => {
|
||||
const layerTypeName = String(layer?.get?.("lyrTyNm") ?? "").trim();
|
||||
if (layerTypeName !== "tb_ms_swage_ppl") return;
|
||||
|
||||
const source = layer.getSource?.();
|
||||
if (source?.updateParams && filter) {
|
||||
const params = { ...(source.getParams?.() ?? {}) };
|
||||
if (!gisEditOriginalLayerFilterRef.current.has(layer)) {
|
||||
gisEditOriginalLayerFilterRef.current.set(
|
||||
layer,
|
||||
String(params.CQL_FILTER ?? "").trim() || null,
|
||||
);
|
||||
}
|
||||
const originalFilter =
|
||||
gisEditOriginalLayerFilterRef.current.get(layer);
|
||||
params.CQL_FILTER = originalFilter
|
||||
? `(${originalFilter}) AND (${filter})`
|
||||
: filter;
|
||||
params._t = Date.now();
|
||||
source.updateParams(params);
|
||||
source.refresh?.();
|
||||
source.changed?.();
|
||||
layer.changed?.();
|
||||
}
|
||||
|
||||
source?.getFeatures?.().forEach((feature: any) => {
|
||||
const fid = getGisEditFeatureFid(feature);
|
||||
const shouldHide = hiddenFidSet.has(fid);
|
||||
if (
|
||||
shouldHide &&
|
||||
!gisEditOriginalFeatureStyleRef.current.has(feature)
|
||||
) {
|
||||
gisEditOriginalFeatureStyleRef.current.set(
|
||||
feature,
|
||||
feature.getStyle?.(),
|
||||
);
|
||||
feature.setStyle?.(() => []);
|
||||
feature.changed?.();
|
||||
}
|
||||
if (
|
||||
!shouldHide &&
|
||||
gisEditOriginalFeatureStyleRef.current.has(feature)
|
||||
) {
|
||||
feature.setStyle?.(
|
||||
gisEditOriginalFeatureStyleRef.current.get(feature),
|
||||
);
|
||||
gisEditOriginalFeatureStyleRef.current.delete(feature);
|
||||
feature.changed?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gisMaps.current?.renderSync?.();
|
||||
},
|
||||
[gisMaps, restoreGisEditOriginalPipeMask],
|
||||
);
|
||||
|
||||
const gisEditMaskedPipeFidKey = useMemo(() => {
|
||||
const fids = editChanges
|
||||
.filter((change) => change.layerId === "tb_ms_swage_ppl")
|
||||
.filter((change) => !change.attributeOnly)
|
||||
.filter(
|
||||
(change) => change.editType !== "ADD" && change.editType !== "DELETE",
|
||||
)
|
||||
.map((change) =>
|
||||
normalizeGisEditFid(
|
||||
change.fid ?? change.featureRef?.fid ?? change.objectId,
|
||||
),
|
||||
)
|
||||
.filter((fid) => /^\d+$/.test(fid));
|
||||
return Array.from(new Set(fids)).sort().join(",");
|
||||
}, [editChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !editIsEditMode) {
|
||||
restoreGisEditOriginalPipeMask();
|
||||
return;
|
||||
}
|
||||
|
||||
applyGisEditOriginalPipeMask(
|
||||
gisEditMaskedPipeFidKey ? gisEditMaskedPipeFidKey.split(",") : [],
|
||||
);
|
||||
}, [
|
||||
applyGisEditOriginalPipeMask,
|
||||
editIsEditMode,
|
||||
gisEditMaskedPipeFidKey,
|
||||
mapReady,
|
||||
restoreGisEditOriginalPipeMask,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
restoreGisEditOriginalPipeMask();
|
||||
},
|
||||
[restoreGisEditOriginalPipeMask],
|
||||
);
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import { unByKey } from "ol/Observable.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import { FLOW_DIRECTION_SRS } from "../model/map-constants";
|
||||
import { getFeatureCollection } from "./map-values";
|
||||
import {
|
||||
expandGisEditPipeSnapExtent,
|
||||
normalizeGisEditFid,
|
||||
getGisEditFeatureFid,
|
||||
} from "./gis-edit-features";
|
||||
|
||||
/** 편집 스냅용 관로의 영역 조회와 취소 수명을 소유한다. */
|
||||
type Options = {
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
gisMaps: React.RefObject<any>;
|
||||
mapReady: boolean;
|
||||
};
|
||||
|
||||
/** 편집 스냅용 관로의 영역 조회와 취소 수명을 소유한다. */
|
||||
export function useGisEditPipeSnap({ gisEdit, gisMaps, mapReady }: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const {
|
||||
changes: editChanges,
|
||||
isEditMode: editIsEditMode,
|
||||
isSnapEnabled: editIsSnapEnabled,
|
||||
selectedLayer: editSelectedLayer,
|
||||
pendingInteraction: editPendingInteraction,
|
||||
} = gisEdit;
|
||||
|
||||
const gisEditPipeSnapSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const gisEditPipeSnapAbortControllerRef = useRef<AbortController | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const gisEditPipeSnapRequestSeqRef = useRef(0);
|
||||
|
||||
const ensureGisEditPipeSnapSource = useCallback(() => {
|
||||
if (gisEditPipeSnapSourceRef.current) {
|
||||
return gisEditPipeSnapSourceRef.current;
|
||||
}
|
||||
const source = new VectorSource();
|
||||
gisEditPipeSnapSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const clearGisEditPipeSnapSource = useCallback(() => {
|
||||
gisEditPipeSnapAbortControllerRef.current?.abort();
|
||||
gisEditPipeSnapAbortControllerRef.current = null;
|
||||
gisEditPipeSnapSourceRef.current?.clear();
|
||||
}, []);
|
||||
|
||||
const buildGisEditPipeSnapPreviewFeatures = useCallback(
|
||||
(excludedChangeId?: string | null) => {
|
||||
const normalizedExcludedChangeId = String(excludedChangeId ?? "").trim();
|
||||
const geoJson = new GeoJSON();
|
||||
|
||||
return editChanges
|
||||
.filter((change) => change.layerId === "tb_ms_swage_ppl")
|
||||
.filter(
|
||||
(change) =>
|
||||
!change.attributeOnly &&
|
||||
change.editType !== "DELETE" &&
|
||||
Boolean(change.afterGeometry),
|
||||
)
|
||||
.filter((change) => change.changeId !== normalizedExcludedChangeId)
|
||||
.map((change) => {
|
||||
const feature = geoJson.readFeature(
|
||||
{
|
||||
type: "Feature",
|
||||
geometry: change.afterGeometry,
|
||||
properties: {
|
||||
fid:
|
||||
change.fid ??
|
||||
change.featureRef?.fid ??
|
||||
change.objectId ??
|
||||
change.changeId,
|
||||
layerId: change.layerId,
|
||||
changeId: change.changeId,
|
||||
},
|
||||
},
|
||||
{
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
},
|
||||
);
|
||||
feature.setId(`gis-edit-snap:${change.changeId}`);
|
||||
return feature;
|
||||
});
|
||||
},
|
||||
[editChanges],
|
||||
);
|
||||
|
||||
const loadGisEditPipeSnapSource = useCallback(
|
||||
async (options?: {
|
||||
excludedFid?: string | null;
|
||||
excludedChangeId?: string | null;
|
||||
}) => {
|
||||
const map = gisMaps.current;
|
||||
const source = ensureGisEditPipeSnapSource();
|
||||
if (!map || !source) return;
|
||||
|
||||
const size = map.getSize?.();
|
||||
const view = map.getView?.();
|
||||
if (!size || !view?.calculateExtent) return;
|
||||
|
||||
const normalizedExcludedFid = normalizeGisEditFid(
|
||||
options?.excludedFid ?? "",
|
||||
);
|
||||
const normalizedExcludedChangeId = String(
|
||||
options?.excludedChangeId ?? "",
|
||||
).trim();
|
||||
const hiddenPipeFidSet = new Set(
|
||||
editChanges
|
||||
.filter((change) => change.layerId === "tb_ms_swage_ppl")
|
||||
.filter(
|
||||
(change) => !change.attributeOnly && change.editType !== "DELETE",
|
||||
)
|
||||
.map((change) =>
|
||||
normalizeGisEditFid(
|
||||
change.fid ?? change.featureRef?.fid ?? change.objectId,
|
||||
),
|
||||
)
|
||||
.filter((fid) => /^\d+$/.test(fid)),
|
||||
);
|
||||
if (/^\d+$/.test(normalizedExcludedFid)) {
|
||||
hiddenPipeFidSet.add(normalizedExcludedFid);
|
||||
}
|
||||
|
||||
const previewSnapFeatures = buildGisEditPipeSnapPreviewFeatures(
|
||||
normalizedExcludedChangeId,
|
||||
);
|
||||
source.clear();
|
||||
if (previewSnapFeatures.length > 0) {
|
||||
source.addFeatures(previewSnapFeatures);
|
||||
}
|
||||
|
||||
const screenExtent = view.calculateExtent(size);
|
||||
const requestExtent = expandGisEditPipeSnapExtent(screenExtent);
|
||||
const [minX, minY, maxX, maxY] = requestExtent;
|
||||
const bboxFilter = `BBOX(geom, ${minX}, ${minY}, ${maxX}, ${maxY}, '${GisApp.TO_PROJ}')`;
|
||||
const cqlFilter = bboxFilter;
|
||||
|
||||
const requestSeq = gisEditPipeSnapRequestSeqRef.current + 1;
|
||||
gisEditPipeSnapRequestSeqRef.current = requestSeq;
|
||||
gisEditPipeSnapAbortControllerRef.current?.abort();
|
||||
const abortController = new AbortController();
|
||||
gisEditPipeSnapAbortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
const layerModule = GisApp.Module.layer as any;
|
||||
const storageName = String(layerModule?.storageNm ?? "hasudo:");
|
||||
const params = new URLSearchParams({
|
||||
SERVICE: "WFS",
|
||||
VERSION: "1.1.0",
|
||||
REQUEST: "GetFeature",
|
||||
TYPENAME: `${storageName}tb_ms_swage_ppl`,
|
||||
SRSNAME: GisApp.TO_PROJ,
|
||||
OUTPUTFORMAT: "application/json",
|
||||
CQL_FILTER: cqlFilter,
|
||||
});
|
||||
const response = await getRawJson(
|
||||
`${GisApp.GEOSERVER_URL}?${params.toString()}`,
|
||||
{
|
||||
signal: abortController.signal,
|
||||
},
|
||||
);
|
||||
if (requestSeq !== gisEditPipeSnapRequestSeqRef.current) return;
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
const featureCollection = getFeatureCollection(response);
|
||||
const features = new GeoJSON()
|
||||
.readFeatures(featureCollection, {
|
||||
dataProjection: GisApp.TO_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
})
|
||||
.filter(
|
||||
(feature: any) =>
|
||||
!hiddenPipeFidSet.has(getGisEditFeatureFid(feature)),
|
||||
);
|
||||
|
||||
source.clear();
|
||||
source.addFeatures(features);
|
||||
if (previewSnapFeatures.length > 0) {
|
||||
source.addFeatures(previewSnapFeatures);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error)?.name === "AbortError") return;
|
||||
console.error("GIS 편집 스냅용 관로 조회 실패:", error);
|
||||
source.clear();
|
||||
if (previewSnapFeatures.length > 0) {
|
||||
source.addFeatures(previewSnapFeatures);
|
||||
}
|
||||
} finally {
|
||||
if (gisEditPipeSnapAbortControllerRef.current === abortController) {
|
||||
gisEditPipeSnapAbortControllerRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
gisMaps,
|
||||
ensureGisEditPipeSnapSource,
|
||||
editChanges,
|
||||
buildGisEditPipeSnapPreviewFeatures,
|
||||
],
|
||||
);
|
||||
|
||||
const shouldUsePipeSnapSource =
|
||||
editIsEditMode &&
|
||||
editIsSnapEnabled &&
|
||||
editSelectedLayer?.layerId === "tb_ms_swage_ppl";
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearGisEditPipeSnapSource();
|
||||
},
|
||||
[clearGisEditPipeSnapSource],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
if (!shouldUsePipeSnapSource) {
|
||||
clearGisEditPipeSnapSource();
|
||||
return;
|
||||
}
|
||||
|
||||
const excludedFid =
|
||||
editPendingInteraction?.editType === "VERTEX_EDIT" ||
|
||||
editPendingInteraction?.editType === "MOVE"
|
||||
? normalizeGisEditFid(
|
||||
editChanges.find(
|
||||
(change) => change.changeId === editPendingInteraction?.changeId,
|
||||
)?.fid ?? "",
|
||||
)
|
||||
: "";
|
||||
|
||||
void loadGisEditPipeSnapSource({
|
||||
excludedFid,
|
||||
excludedChangeId: editPendingInteraction?.changeId ?? null,
|
||||
});
|
||||
}, [
|
||||
clearGisEditPipeSnapSource,
|
||||
editChanges,
|
||||
editPendingInteraction,
|
||||
gisMaps,
|
||||
loadGisEditPipeSnapSource,
|
||||
mapReady,
|
||||
shouldUsePipeSnapSource,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
if (!shouldUsePipeSnapSource) return;
|
||||
|
||||
const key = gisMaps.current.on("moveend", () => {
|
||||
const excludedFid =
|
||||
editPendingInteraction?.editType === "VERTEX_EDIT" ||
|
||||
editPendingInteraction?.editType === "MOVE"
|
||||
? normalizeGisEditFid(
|
||||
editChanges.find(
|
||||
(change) =>
|
||||
change.changeId === editPendingInteraction?.changeId,
|
||||
)?.fid ?? "",
|
||||
)
|
||||
: "";
|
||||
void loadGisEditPipeSnapSource({
|
||||
excludedFid,
|
||||
excludedChangeId: editPendingInteraction?.changeId ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unByKey(key);
|
||||
};
|
||||
}, [
|
||||
editChanges,
|
||||
editPendingInteraction,
|
||||
gisMaps,
|
||||
loadGisEditPipeSnapSource,
|
||||
mapReady,
|
||||
shouldUsePipeSnapSource,
|
||||
]);
|
||||
return { ensureGisEditPipeSnapSource, shouldUsePipeSnapSource };
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { createGisEditPreviewStyle } from "@/features/gis-edit/lib/preview-styles";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import {
|
||||
FLOW_DIRECTION_SRS,
|
||||
GIS_EDIT_PREVIEW_LAYER_ID,
|
||||
} from "../model/map-constants";
|
||||
|
||||
/** 편집 미리보기 도형의 표시와 선택 강조 수명을 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
mapReady: boolean;
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
focusGisEditChangeOnMap: (changeId: string) => void;
|
||||
};
|
||||
|
||||
/** 편집 미리보기 도형의 표시와 선택 강조 수명을 소유한다. */
|
||||
export function useGisEditPreview({
|
||||
gisMaps,
|
||||
mapReady,
|
||||
gisEdit,
|
||||
focusGisEditChangeOnMap,
|
||||
}: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const {
|
||||
changes: editChanges,
|
||||
editableLayers: editEditableLayers,
|
||||
pendingInteraction: editPendingInteraction,
|
||||
focusedChangeId: editFocusedChangeId,
|
||||
isEditMode: editIsEditMode,
|
||||
} = gisEdit;
|
||||
|
||||
const gisEditPreviewLayerRef = useRef<any | null>(null);
|
||||
|
||||
const gisEditPreviewSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
// 신규 관로마다 한 번만 우측 상세 패널을 피한 가시 영역으로 이동하기 위한 change 식별자다.
|
||||
const gisEditAutoFocusedAddChangeIdRef = useRef("");
|
||||
|
||||
const setGisEditPreviewCursor = useCallback(
|
||||
(cursor: string) => {
|
||||
const targetElement = gisMaps.current?.getTargetElement?.();
|
||||
if (targetElement?.style) {
|
||||
targetElement.style.cursor = cursor;
|
||||
}
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
|
||||
const ensureGisEditPreviewLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (gisEditPreviewLayerRef.current && gisEditPreviewSourceRef.current) {
|
||||
const layers = map.getLayers?.().getArray?.() ?? [];
|
||||
if (!layers.includes(gisEditPreviewLayerRef.current)) {
|
||||
map.addLayer(gisEditPreviewLayerRef.current);
|
||||
}
|
||||
gisEditPreviewLayerRef.current.setVisible?.(true);
|
||||
gisEditPreviewLayerRef.current.setZIndex?.(10001);
|
||||
return gisEditPreviewSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
style: createGisEditPreviewStyle,
|
||||
zIndex: 10001,
|
||||
updateWhileAnimating: true,
|
||||
updateWhileInteracting: true,
|
||||
});
|
||||
|
||||
layer.set("id", GIS_EDIT_PREVIEW_LAYER_ID);
|
||||
layer.set("lyrNm", "GIS 편집 미리보기");
|
||||
layer.set("lyrTyNm", GIS_EDIT_PREVIEW_LAYER_ID);
|
||||
// Preview selection is handled by map.forEachFeatureAtPixel; keep it out of the common Select styler.
|
||||
layer.set("clickPsblYn", "N");
|
||||
layer.setVisible?.(true);
|
||||
|
||||
map.addLayer(layer);
|
||||
gisEditPreviewLayerRef.current = layer;
|
||||
gisEditPreviewSourceRef.current = source;
|
||||
return source;
|
||||
}, [gisMaps]);
|
||||
|
||||
const clearGisEditPreviewLayer = useCallback(() => {
|
||||
gisEditPreviewSourceRef.current?.clear();
|
||||
setGisEditPreviewCursor("");
|
||||
}, [setGisEditPreviewCursor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const source = ensureGisEditPreviewLayer();
|
||||
if (!source) return;
|
||||
|
||||
if (editChanges.length === 0) {
|
||||
clearGisEditPreviewLayer();
|
||||
return;
|
||||
}
|
||||
|
||||
const geoJson = new GeoJSON();
|
||||
const layerPreviewColorMap = new globalThis.Map<string, any>(
|
||||
editEditableLayers.map(
|
||||
(layer) => [layer.layerId, layer.previewColors] as const,
|
||||
),
|
||||
);
|
||||
const existingFeatureMap = new globalThis.Map<string, any>(
|
||||
source
|
||||
.getFeatures()
|
||||
.map(
|
||||
(feature: any) =>
|
||||
[String(feature.get("changeId") ?? ""), feature] as const,
|
||||
),
|
||||
);
|
||||
const nextChangeIdSet = new Set<string>();
|
||||
|
||||
editChanges.forEach((change) => {
|
||||
if (change.attributeOnly) return;
|
||||
|
||||
const geometry =
|
||||
change.editType === "DELETE"
|
||||
? change.beforeGeometry
|
||||
: change.afterGeometry;
|
||||
if (!geometry) return;
|
||||
|
||||
const previewEditType =
|
||||
editPendingInteraction?.changeId === change.changeId
|
||||
? editPendingInteraction.editType
|
||||
: change.editType;
|
||||
const previewLabel =
|
||||
editFocusedChangeId === change.changeId && previewEditType === "MOVE"
|
||||
? "드래그 이동"
|
||||
: editFocusedChangeId === change.changeId &&
|
||||
previewEditType === "VERTEX_EDIT"
|
||||
? "꼭지점 편집"
|
||||
: previewEditType === "DELETE"
|
||||
? "삭제 예정"
|
||||
: previewEditType === "MOVE"
|
||||
? "이동 예정"
|
||||
: previewEditType === "VERTEX_EDIT"
|
||||
? "편집 예정"
|
||||
: "신규 추가";
|
||||
|
||||
let feature = existingFeatureMap.get(change.changeId);
|
||||
const previewColors = layerPreviewColorMap.get(change.layerId);
|
||||
const originalProperties = change.featureProperties ?? {};
|
||||
const previewObjectId = change.objectId ?? change.objectLabel;
|
||||
const previewFid =
|
||||
originalProperties["fid"] ??
|
||||
change.fid ??
|
||||
change.featureRef?.fid ??
|
||||
previewObjectId;
|
||||
if (!feature) {
|
||||
feature = geoJson.readFeature(
|
||||
{
|
||||
type: "Feature",
|
||||
geometry,
|
||||
properties: {
|
||||
...originalProperties,
|
||||
fid: previewFid,
|
||||
editType: previewEditType,
|
||||
objectId: previewObjectId,
|
||||
layerId: change.layerId,
|
||||
changeId: change.changeId,
|
||||
},
|
||||
},
|
||||
{
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
},
|
||||
);
|
||||
feature.setId(`gis-edit:${change.changeId}`);
|
||||
source.addFeature(feature);
|
||||
} else {
|
||||
feature.setGeometry(
|
||||
geoJson.readGeometry(geometry, {
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Object.entries(originalProperties).forEach(([key, value]) => {
|
||||
if (key === "geometry") return;
|
||||
feature.set(key, value);
|
||||
});
|
||||
feature.set("fid", previewFid);
|
||||
feature.set("editType", previewEditType);
|
||||
feature.set("objectId", previewObjectId);
|
||||
feature.set("layerId", change.layerId);
|
||||
feature.set("changeId", change.changeId);
|
||||
feature.set("layerPointColor", previewColors?.pointColor ?? "");
|
||||
feature.set("layerLineColor", previewColors?.lineColor ?? "");
|
||||
feature.set("layerFillColor", previewColors?.fillColor ?? "");
|
||||
feature.set(
|
||||
"isInteractionTarget",
|
||||
editFocusedChangeId === change.changeId,
|
||||
);
|
||||
feature.set("isDragging", Boolean(feature.get("isDragging")));
|
||||
feature.set("previewLabel", previewLabel);
|
||||
feature.setStyle(createGisEditPreviewStyle(feature));
|
||||
nextChangeIdSet.add(change.changeId);
|
||||
});
|
||||
|
||||
existingFeatureMap.forEach((feature: any, changeId: string) => {
|
||||
if (!nextChangeIdSet.has(changeId)) {
|
||||
source.removeFeature(feature);
|
||||
}
|
||||
});
|
||||
source.changed?.();
|
||||
gisEditPreviewLayerRef.current?.changed?.();
|
||||
gisMaps.current?.renderSync?.();
|
||||
}, [
|
||||
clearGisEditPreviewLayer,
|
||||
ensureGisEditPreviewLayer,
|
||||
editChanges,
|
||||
editEditableLayers,
|
||||
editFocusedChangeId,
|
||||
editIsEditMode,
|
||||
editPendingInteraction,
|
||||
mapReady,
|
||||
gisMaps,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !editIsEditMode || editChanges.length === 0) return;
|
||||
ensureGisEditPreviewLayer();
|
||||
gisEditPreviewSourceRef.current?.changed?.();
|
||||
gisEditPreviewLayerRef.current?.changed?.();
|
||||
gisMaps.current?.renderSync?.();
|
||||
}, [
|
||||
ensureGisEditPreviewLayer,
|
||||
editChanges.length,
|
||||
editIsEditMode,
|
||||
mapReady,
|
||||
gisMaps,
|
||||
]);
|
||||
|
||||
// ADD 완료 직후 열린 상세 패널 아래에 신규 선형이 가려지지 않도록 최초 1회 지도 가시 영역에 맞춘다.
|
||||
useEffect(() => {
|
||||
if (!mapReady || !editIsEditMode || !editFocusedChangeId) return;
|
||||
|
||||
const focusedChange = editChanges.find(
|
||||
(change) => change.changeId === editFocusedChangeId,
|
||||
);
|
||||
if (
|
||||
focusedChange?.editType !== "ADD" ||
|
||||
!focusedChange.afterGeometry ||
|
||||
gisEditAutoFocusedAddChangeIdRef.current === focusedChange.changeId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
gisEditAutoFocusedAddChangeIdRef.current = focusedChange.changeId;
|
||||
const focusTimer = window.setTimeout(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
ensureGisEditPreviewLayer();
|
||||
map.updateSize?.();
|
||||
focusGisEditChangeOnMap(focusedChange.changeId);
|
||||
gisEditPreviewSourceRef.current?.changed?.();
|
||||
gisEditPreviewLayerRef.current?.changed?.();
|
||||
map.renderSync?.();
|
||||
}, 120);
|
||||
|
||||
return () => window.clearTimeout(focusTimer);
|
||||
}, [
|
||||
ensureGisEditPreviewLayer,
|
||||
focusGisEditChangeOnMap,
|
||||
editChanges,
|
||||
editFocusedChangeId,
|
||||
editIsEditMode,
|
||||
mapReady,
|
||||
gisMaps,
|
||||
]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
setGisEditPreviewCursor("");
|
||||
if (gisEditPreviewLayerRef.current)
|
||||
gisMaps.current?.removeLayer?.(gisEditPreviewLayerRef.current);
|
||||
gisEditPreviewSourceRef.current?.clear();
|
||||
gisEditPreviewLayerRef.current = null;
|
||||
gisEditPreviewSourceRef.current = null;
|
||||
},
|
||||
[gisMaps, setGisEditPreviewCursor],
|
||||
);
|
||||
return {
|
||||
gisEditPreviewLayerRef,
|
||||
gisEditPreviewSourceRef,
|
||||
setGisEditPreviewCursor,
|
||||
ensureGisEditPreviewLayer,
|
||||
clearGisEditPreviewLayer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
import type * as React from "react";
|
||||
import { type VertexHandleMeta } from "@/features/gis-edit/model/vertex-handles";
|
||||
import { getClosestGeometryVertexMeta } from "@/features/gis-edit/lib/geometry-mutations";
|
||||
import { useEffect } from "react";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import { unByKey } from "ol/Observable.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import { getSelectedFeatureLayerTypeName } from "./right-panel-features";
|
||||
|
||||
/** 편집 중 지도 클릭 우선순위와 공통 선택 interaction 활성 상태를 연결한다. */
|
||||
type Options = {
|
||||
mapReady: boolean;
|
||||
gisMaps: React.RefObject<any>;
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
gisEditVertexLayerRef: React.RefObject<VectorLayer<any> | null>;
|
||||
clearSelectedVertexHandle: () => void;
|
||||
gisEditSelectedVertexHandleKeyRef: React.RefObject<string>;
|
||||
gisEditSelectedVertexMetaListRef: React.RefObject<VertexHandleMeta[] | null>;
|
||||
applySelectedVertexHandleStyle: () => void;
|
||||
setGisEditDetailChangeId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setFeatureData: React.Dispatch<any>;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsSearchModalOpen: (open: boolean) => void;
|
||||
gisEditMoveGripMetaRef: React.RefObject<{
|
||||
changeId: string;
|
||||
meta: VertexHandleMeta | null;
|
||||
} | null>;
|
||||
openGisEditPipeDetail: (changeId: string | null) => void;
|
||||
};
|
||||
|
||||
/** 편집 중 지도 클릭 우선순위와 공통 선택 interaction 활성 상태를 연결한다. */
|
||||
export function useGisEditSelection({
|
||||
mapReady,
|
||||
gisMaps,
|
||||
gisEdit,
|
||||
gisEditVertexLayerRef,
|
||||
clearSelectedVertexHandle,
|
||||
gisEditSelectedVertexHandleKeyRef,
|
||||
gisEditSelectedVertexMetaListRef,
|
||||
applySelectedVertexHandleStyle,
|
||||
setGisEditDetailChangeId,
|
||||
setFeatureData,
|
||||
setIsOpen,
|
||||
setIsSearchModalOpen,
|
||||
gisEditMoveGripMetaRef,
|
||||
openGisEditPipeDetail,
|
||||
}: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const {
|
||||
isEditMode: editIsEditMode,
|
||||
pendingInteraction: editPendingInteraction,
|
||||
announceStatus: editAnnounceStatus,
|
||||
activeTool: editActiveTool,
|
||||
selectedLayer: editSelectedLayer,
|
||||
registerFeatureSelection: editRegisterFeatureSelection,
|
||||
changes: editChanges,
|
||||
} = gisEdit;
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current || !editIsEditMode) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const clickKey = map.on("singleclick", (event: any) => {
|
||||
const vertexHandleFeature =
|
||||
editPendingInteraction?.editType === "VERTEX_EDIT"
|
||||
? map.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
(feature: any, layer: any) =>
|
||||
layer === gisEditVertexLayerRef.current ? feature : null,
|
||||
{ hitTolerance: 14 },
|
||||
)
|
||||
: null;
|
||||
if (vertexHandleFeature) {
|
||||
event.preventDefault?.();
|
||||
const handleKind = String(
|
||||
vertexHandleFeature.get?.("handleKind") ?? "",
|
||||
);
|
||||
if (handleKind !== "vertex") {
|
||||
clearSelectedVertexHandle();
|
||||
editAnnounceStatus(
|
||||
"흰색 midpoint는 드래그해서 새 꼭지점을 추가할 수 있습니다. Delete 삭제 대상은 주황색 실꼭지점입니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
gisEditSelectedVertexHandleKeyRef.current = String(
|
||||
vertexHandleFeature.get?.("handleSelectionKey") ?? "",
|
||||
);
|
||||
gisEditSelectedVertexMetaListRef.current =
|
||||
(vertexHandleFeature.get?.("handleMetaList") as
|
||||
| VertexHandleMeta[]
|
||||
| undefined) ?? null;
|
||||
applySelectedVertexHandleStyle();
|
||||
editAnnounceStatus(
|
||||
"주황색 실꼭지점을 선택했습니다. Delete 키를 누르면 해당 꼭지점을 삭제합니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewFeature = map.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
(feature: any) =>
|
||||
String(feature?.get?.("changeId") ?? "").trim() ? feature : null,
|
||||
{ hitTolerance: 12 },
|
||||
);
|
||||
// 편집 도구가 켜진 동안에는 공통 Select를 우회하므로 원본 WFS 피처도 여기서 직접 판정한다.
|
||||
const editableSourceFeature = previewFeature
|
||||
? null
|
||||
: editActiveTool && editActiveTool !== "ADD"
|
||||
? map.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
(feature: any) => {
|
||||
if (String(feature?.get?.("changeId") ?? "").trim())
|
||||
return null;
|
||||
return getSelectedFeatureLayerTypeName(feature) ===
|
||||
editSelectedLayer?.layerId
|
||||
? feature
|
||||
: null;
|
||||
},
|
||||
{ hitTolerance: 12 },
|
||||
)
|
||||
: null;
|
||||
const selectedFeature = previewFeature ?? editableSourceFeature;
|
||||
if (!selectedFeature) return;
|
||||
clearSelectedVertexHandle();
|
||||
|
||||
const editSelection = editRegisterFeatureSelection(selectedFeature);
|
||||
if (!editSelection.handled) return;
|
||||
|
||||
event.preventDefault?.();
|
||||
|
||||
if (editSelection.editType === "DELETE") {
|
||||
const select = GisApp.Module.select as any;
|
||||
select?.resetSelectedFeature?.();
|
||||
setGisEditDetailChangeId(null);
|
||||
setFeatureData(null);
|
||||
setIsOpen(false);
|
||||
setIsSearchModalOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editActiveTool === "MOVE" || editActiveTool === "VERTEX_EDIT") {
|
||||
if (
|
||||
editActiveTool === "MOVE" &&
|
||||
typeof editSelection.changeId === "string" &&
|
||||
editSelection.changeId
|
||||
) {
|
||||
gisEditMoveGripMetaRef.current = {
|
||||
changeId: String(editSelection.changeId),
|
||||
meta: getClosestGeometryVertexMeta(
|
||||
selectedFeature.getGeometry?.(),
|
||||
map,
|
||||
event.coordinate,
|
||||
),
|
||||
};
|
||||
}
|
||||
setGisEditDetailChangeId(null);
|
||||
setFeatureData(null);
|
||||
setIsOpen(false);
|
||||
setIsSearchModalOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedChangeId =
|
||||
typeof editSelection.changeId === "string"
|
||||
? editSelection.changeId
|
||||
: "";
|
||||
const selectedChange = editChanges.find(
|
||||
(change) => change.changeId === selectedChangeId,
|
||||
);
|
||||
const canOpenPipeDetail =
|
||||
selectedChangeId && editSelection.editType !== "DELETE";
|
||||
|
||||
if (canOpenPipeDetail) {
|
||||
openGisEditPipeDetail(selectedChangeId);
|
||||
setFeatureData(
|
||||
selectedChange?.editType === "ADD"
|
||||
? {
|
||||
lyrTypeNm: "tb_ms_swage_ppl",
|
||||
fcltMngNo: "",
|
||||
mngInstCd: "",
|
||||
fclt_mng_no: "",
|
||||
inst_mng_no: "",
|
||||
}
|
||||
: null,
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
setGisEditDetailChangeId(selectedChangeId);
|
||||
setIsOpen(true);
|
||||
}, 0);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unByKey(clickKey);
|
||||
};
|
||||
}, [
|
||||
applySelectedVertexHandleStyle,
|
||||
clearSelectedVertexHandle,
|
||||
editActiveTool,
|
||||
editChanges,
|
||||
editIsEditMode,
|
||||
editPendingInteraction,
|
||||
editAnnounceStatus,
|
||||
editRegisterFeatureSelection,
|
||||
editSelectedLayer,
|
||||
mapReady,
|
||||
openGisEditPipeDetail,
|
||||
gisMaps,
|
||||
gisEditVertexLayerRef,
|
||||
gisEditSelectedVertexHandleKeyRef,
|
||||
gisEditSelectedVertexMetaListRef,
|
||||
setGisEditDetailChangeId,
|
||||
setFeatureData,
|
||||
setIsOpen,
|
||||
setIsSearchModalOpen,
|
||||
gisEditMoveGripMetaRef,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const select = GisApp.Module.select as any;
|
||||
if (!select?.setActive) return;
|
||||
|
||||
if (editIsEditMode && editActiveTool) {
|
||||
select.resetSelectedFeature?.();
|
||||
select.setActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
select.setActive(true);
|
||||
}, [editActiveTool, editIsEditMode, mapReady]);
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
"use client";
|
||||
import type Geometry from "ol/geom/Geometry.js";
|
||||
import type * as React from "react";
|
||||
import {
|
||||
type VertexHandleMeta,
|
||||
type VertexHandleCandidate,
|
||||
} from "@/features/gis-edit/model/vertex-handles";
|
||||
import {
|
||||
createGisEditPreviewStyle,
|
||||
GIS_EDIT_VERTEX_DRAGGING_STYLE,
|
||||
getGisEditVertexHandleStyle,
|
||||
} from "@/features/gis-edit/lib/preview-styles";
|
||||
import {
|
||||
createVertexHandleFeature,
|
||||
buildVertexHandleFeatures,
|
||||
findHoveredMidpointCandidate,
|
||||
} from "@/features/gis-edit/lib/vertex-handles";
|
||||
import {
|
||||
applyVertexHandleMove,
|
||||
deleteSelectedVertexHandles,
|
||||
applyGeometryMoveSnap,
|
||||
getClosestGeometryVertexCoordinate,
|
||||
getGeometryVertexCoordinateByMeta,
|
||||
} from "@/features/gis-edit/lib/geometry-mutations";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import Collection from "ol/Collection.js";
|
||||
import Feature from "ol/Feature.js";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import Snap from "ol/interaction/Snap.js";
|
||||
import Translate from "ol/interaction/Translate.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import { unByKey } from "ol/Observable.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useGisEdit } from "../components/gis-edit/use-gis-edit";
|
||||
import {
|
||||
FLOW_DIRECTION_SRS,
|
||||
GIS_EDIT_VERTEX_LAYER_ID,
|
||||
} from "../model/map-constants";
|
||||
import { geometryObjectToKey, isTypingElement } from "./map-values";
|
||||
import { normalizeGisEditFid, getGisEditFeatureFid } from "./gis-edit-features";
|
||||
import { getSelectedFeatureLayerTypeName } from "./right-panel-features";
|
||||
|
||||
/** 이동·꼭지점 편집 interaction과 선택 핸들의 생성·정리를 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
gisEditPreviewSourceRef: React.RefObject<VectorSource<Geometry> | null>;
|
||||
gisEdit: ReturnType<typeof useGisEdit>;
|
||||
gisEditPreviewLayerRef: React.RefObject<any>;
|
||||
setGisEditPreviewCursor: (cursor: string) => void;
|
||||
mapReady: boolean;
|
||||
ensureGisEditPreviewLayer: () => VectorSource<Geometry> | null;
|
||||
shouldUsePipeSnapSource: boolean;
|
||||
ensureGisEditPipeSnapSource: () => VectorSource<Geometry>;
|
||||
gisEditMoveGripMetaRef: React.RefObject<{
|
||||
changeId: string;
|
||||
meta: VertexHandleMeta | null;
|
||||
} | null>;
|
||||
};
|
||||
|
||||
/** 이동·꼭지점 편집 interaction과 선택 핸들의 생성·정리를 소유한다. */
|
||||
export function useGisEditTransform({
|
||||
gisMaps,
|
||||
gisEditPreviewSourceRef,
|
||||
gisEdit,
|
||||
gisEditPreviewLayerRef,
|
||||
setGisEditPreviewCursor,
|
||||
mapReady,
|
||||
ensureGisEditPreviewLayer,
|
||||
shouldUsePipeSnapSource,
|
||||
ensureGisEditPipeSnapSource,
|
||||
gisEditMoveGripMetaRef,
|
||||
}: Options) {
|
||||
// 편집 세션 전체 객체 대신 실제 사용하는 상태와 명령만 의존성으로 추적한다.
|
||||
const {
|
||||
selectedLayer: editSelectedLayer,
|
||||
changes: editChanges,
|
||||
isEditMode: editIsEditMode,
|
||||
pendingInteraction: editPendingInteraction,
|
||||
clearPendingInteraction: editClearPendingInteraction,
|
||||
isSnapEnabled: editIsSnapEnabled,
|
||||
updateChangeGeometry: editUpdateChangeGeometry,
|
||||
announceStatus: editAnnounceStatus,
|
||||
activeTool: editActiveTool,
|
||||
} = gisEdit;
|
||||
|
||||
// vertex Delete는 현재 선택된 주황색 실꼭지점 묶음을 ref로 기억해 키보드 이벤트에서 즉시 사용한다.
|
||||
const gisEditSelectedVertexHandleKeyRef = useRef("");
|
||||
|
||||
const gisEditSelectedVertexMetaListRef = useRef<VertexHandleMeta[] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const gisEditVertexLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const gisEditVertexSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const gisEditTranslateRef = useRef<any | null>(null);
|
||||
|
||||
const gisEditModifyRef = useRef<any | null>(null);
|
||||
|
||||
// 스냅 interaction은 MOVE/VERTEX_EDIT 진입마다 다시 붙이므로 별도 ref로 정리한다.
|
||||
const gisEditSnapRef = useRef<any | null>(null);
|
||||
|
||||
const ensureGisEditVertexLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (gisEditVertexLayerRef.current && gisEditVertexSourceRef.current) {
|
||||
return gisEditVertexSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 10005,
|
||||
updateWhileAnimating: true,
|
||||
updateWhileInteracting: true,
|
||||
});
|
||||
|
||||
layer.set("id", GIS_EDIT_VERTEX_LAYER_ID);
|
||||
layer.set("lyrNm", "GIS 편집 꼭지점");
|
||||
layer.set("lyrTyNm", GIS_EDIT_VERTEX_LAYER_ID);
|
||||
layer.set("clickPsblYn", "N");
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(GIS_EDIT_VERTEX_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
gisEditVertexLayerRef.current = layer;
|
||||
gisEditVertexSourceRef.current = source;
|
||||
return source;
|
||||
}, [gisMaps]);
|
||||
|
||||
const clearGisEditVertexLayer = useCallback(() => {
|
||||
gisEditVertexSourceRef.current?.clear();
|
||||
gisEditSelectedVertexHandleKeyRef.current = "";
|
||||
gisEditSelectedVertexMetaListRef.current = null;
|
||||
}, []);
|
||||
|
||||
const applySelectedVertexHandleStyle = useCallback(() => {
|
||||
const selectedHandleKey = gisEditSelectedVertexHandleKeyRef.current;
|
||||
const vertexSource = gisEditVertexSourceRef.current;
|
||||
if (!vertexSource) return;
|
||||
|
||||
vertexSource.getFeatures().forEach((feature: any) => {
|
||||
const handleKind = String(
|
||||
feature?.get?.("handleKind") ?? "",
|
||||
) as VertexHandleMeta["kind"];
|
||||
const handleSelectionKey = String(
|
||||
feature?.get?.("handleSelectionKey") ?? "",
|
||||
);
|
||||
const isSelected =
|
||||
Boolean(selectedHandleKey) && handleSelectionKey === selectedHandleKey;
|
||||
feature.setStyle(getGisEditVertexHandleStyle(handleKind, isSelected));
|
||||
});
|
||||
vertexSource.changed?.();
|
||||
}, []);
|
||||
|
||||
const clearSelectedVertexHandle = useCallback(() => {
|
||||
gisEditSelectedVertexHandleKeyRef.current = "";
|
||||
gisEditSelectedVertexMetaListRef.current = null;
|
||||
applySelectedVertexHandleStyle();
|
||||
}, [applySelectedVertexHandleStyle]);
|
||||
|
||||
const collectGisEditSnapFeatures = useCallback(
|
||||
(targetFeature: any) => {
|
||||
const map = gisMaps.current;
|
||||
const previewSource = gisEditPreviewSourceRef.current;
|
||||
const selectedLayerId = editSelectedLayer?.layerId;
|
||||
if (!map || !previewSource || !selectedLayerId) return [] as any[];
|
||||
|
||||
const hiddenFidSet = new Set(
|
||||
editChanges
|
||||
.filter((change) => change.editType === "DELETE")
|
||||
.map((change) =>
|
||||
normalizeGisEditFid(
|
||||
change.fid ?? change.featureRef?.fid ?? change.objectId,
|
||||
),
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
const snapFeatures: any[] = [];
|
||||
const targetChangeId = String(
|
||||
targetFeature?.get?.("changeId") ?? "",
|
||||
).trim();
|
||||
|
||||
const registerFeature = (feature: any) => {
|
||||
if (!feature || feature === targetFeature) return;
|
||||
if (!feature.getGeometry?.()) return;
|
||||
if (getSelectedFeatureLayerTypeName(feature) !== selectedLayerId)
|
||||
return;
|
||||
|
||||
const changeId = String(feature.get?.("changeId") ?? "").trim();
|
||||
if (changeId && changeId === targetChangeId) return;
|
||||
if (String(feature.get?.("editType") ?? "").trim() === "DELETE") return;
|
||||
|
||||
const fid = getGisEditFeatureFid(feature);
|
||||
if (fid && hiddenFidSet.has(fid)) return;
|
||||
|
||||
const objectKey =
|
||||
changeId ||
|
||||
fid ||
|
||||
String(feature.get?.("objectId") ?? feature.getId?.() ?? "");
|
||||
const featureKey = `${selectedLayerId}:${objectKey}`;
|
||||
if (!objectKey || seen.has(featureKey)) return;
|
||||
|
||||
seen.add(featureKey);
|
||||
snapFeatures.push(feature);
|
||||
};
|
||||
|
||||
previewSource.getFeatures().forEach(registerFeature);
|
||||
map.getAllLayers?.().forEach((layer: any) => {
|
||||
if (
|
||||
layer === gisEditPreviewLayerRef.current ||
|
||||
layer === gisEditVertexLayerRef.current
|
||||
)
|
||||
return;
|
||||
const layerTypeName = String(layer?.get?.("lyrTyNm") ?? "").trim();
|
||||
if (layerTypeName !== selectedLayerId) return;
|
||||
layer.getSource?.()?.getFeatures?.().forEach(registerFeature);
|
||||
});
|
||||
|
||||
return snapFeatures;
|
||||
},
|
||||
[
|
||||
editChanges,
|
||||
editSelectedLayer?.layerId,
|
||||
gisEditPreviewLayerRef,
|
||||
gisEditPreviewSourceRef,
|
||||
gisMaps,
|
||||
],
|
||||
);
|
||||
|
||||
const removeGisEditInteractions = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
if (gisEditSnapRef.current) {
|
||||
map.removeInteraction(gisEditSnapRef.current);
|
||||
gisEditSnapRef.current = null;
|
||||
}
|
||||
if (gisEditTranslateRef.current) {
|
||||
map.removeInteraction(gisEditTranslateRef.current);
|
||||
gisEditTranslateRef.current = null;
|
||||
}
|
||||
if (gisEditModifyRef.current) {
|
||||
const modifyInteractions = Array.isArray(gisEditModifyRef.current)
|
||||
? gisEditModifyRef.current
|
||||
: [gisEditModifyRef.current];
|
||||
modifyInteractions.forEach((interaction: any) => {
|
||||
if (interaction) {
|
||||
map.removeInteraction(interaction);
|
||||
}
|
||||
});
|
||||
gisEditModifyRef.current = null;
|
||||
}
|
||||
clearGisEditVertexLayer();
|
||||
setGisEditPreviewCursor("");
|
||||
}, [clearGisEditVertexLayer, gisMaps, setGisEditPreviewCursor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!mapReady ||
|
||||
!gisMaps.current ||
|
||||
!editIsEditMode ||
|
||||
!editPendingInteraction
|
||||
)
|
||||
return;
|
||||
|
||||
const source = ensureGisEditPreviewLayer();
|
||||
const map = gisMaps.current;
|
||||
if (!source || !map) return;
|
||||
|
||||
const targetFeature = source
|
||||
.getFeatures()
|
||||
.find(
|
||||
(feature: any) =>
|
||||
feature.get("changeId") === editPendingInteraction?.changeId,
|
||||
);
|
||||
if (!targetFeature) {
|
||||
editClearPendingInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
removeGisEditInteractions();
|
||||
const features = new Collection([targetFeature]);
|
||||
let cleanupPointerMove: (() => void) | undefined;
|
||||
const writeGeometryObject = () => {
|
||||
const geometry = targetFeature.getGeometry();
|
||||
if (!geometry) return null;
|
||||
return new GeoJSON().writeGeometryObject(geometry, {
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
}) as Record<string, unknown>;
|
||||
};
|
||||
const usePipeSnapSource =
|
||||
shouldUsePipeSnapSource &&
|
||||
(editPendingInteraction.editType === "MOVE" ||
|
||||
editPendingInteraction.editType === "VERTEX_EDIT");
|
||||
const pipeSnapSource = usePipeSnapSource
|
||||
? ensureGisEditPipeSnapSource()
|
||||
: null;
|
||||
const pipeSnapSourceHasFeatures = Boolean(
|
||||
pipeSnapSource?.getFeatures?.().length,
|
||||
);
|
||||
const snapFeatures =
|
||||
(!usePipeSnapSource || !pipeSnapSourceHasFeatures) && editIsSnapEnabled
|
||||
? collectGisEditSnapFeatures(targetFeature)
|
||||
: [];
|
||||
|
||||
if (editPendingInteraction.editType === "MOVE") {
|
||||
const translate = new Translate({ features, hitTolerance: 10 });
|
||||
let lastTranslateCoordinate: number[] | null = null;
|
||||
let translateStartCoordinate: number[] | null = null;
|
||||
let originalMoveGeometrySnapshot =
|
||||
targetFeature.getGeometry?.()?.clone?.() ?? null;
|
||||
let moveGripCoordinate: number[] | null = null;
|
||||
const moveGripMeta =
|
||||
gisEditMoveGripMetaRef.current?.changeId ===
|
||||
editPendingInteraction.changeId
|
||||
? gisEditMoveGripMetaRef.current.meta
|
||||
: null;
|
||||
translate.on("translatestart", (event: any) => {
|
||||
lastTranslateCoordinate = Array.isArray(event?.coordinate)
|
||||
? event.coordinate
|
||||
: null;
|
||||
translateStartCoordinate = Array.isArray(event?.coordinate)
|
||||
? event.coordinate
|
||||
: null;
|
||||
originalMoveGeometrySnapshot =
|
||||
targetFeature.getGeometry?.()?.clone?.() ?? null;
|
||||
moveGripCoordinate =
|
||||
getGeometryVertexCoordinateByMeta(
|
||||
originalMoveGeometrySnapshot,
|
||||
moveGripMeta,
|
||||
) ??
|
||||
getClosestGeometryVertexCoordinate(
|
||||
originalMoveGeometrySnapshot,
|
||||
map,
|
||||
translateStartCoordinate,
|
||||
);
|
||||
targetFeature.set("isDragging", true);
|
||||
targetFeature.set("previewLabel", "이동 중");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
setGisEditPreviewCursor("grabbing");
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
});
|
||||
translate.on("translating", (event: any) => {
|
||||
lastTranslateCoordinate = Array.isArray(event?.coordinate)
|
||||
? event.coordinate
|
||||
: lastTranslateCoordinate;
|
||||
if (
|
||||
originalMoveGeometrySnapshot?.clone &&
|
||||
Array.isArray(lastTranslateCoordinate) &&
|
||||
Array.isArray(translateStartCoordinate)
|
||||
) {
|
||||
const geometry = originalMoveGeometrySnapshot.clone();
|
||||
const deltaX =
|
||||
Number(lastTranslateCoordinate[0] ?? 0) -
|
||||
Number(translateStartCoordinate[0] ?? 0);
|
||||
const deltaY =
|
||||
Number(lastTranslateCoordinate[1] ?? 0) -
|
||||
Number(translateStartCoordinate[1] ?? 0);
|
||||
geometry.translate(deltaX, deltaY);
|
||||
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
|
||||
const movedGripCoordinate = Array.isArray(moveGripCoordinate)
|
||||
? [
|
||||
Number(moveGripCoordinate[0] ?? 0) + deltaX,
|
||||
Number(moveGripCoordinate[1] ?? 0) + deltaY,
|
||||
]
|
||||
: lastTranslateCoordinate;
|
||||
applyGeometryMoveSnap(
|
||||
geometry,
|
||||
pipeSnapSource,
|
||||
map,
|
||||
movedGripCoordinate,
|
||||
28,
|
||||
);
|
||||
}
|
||||
targetFeature.setGeometry(geometry);
|
||||
}
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
});
|
||||
translate.on("translateend", (event: any) => {
|
||||
const objectId = String(targetFeature.get("objectId") ?? "");
|
||||
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
|
||||
lastTranslateCoordinate = Array.isArray(event?.coordinate)
|
||||
? event.coordinate
|
||||
: lastTranslateCoordinate;
|
||||
targetFeature.set("isDragging", false);
|
||||
targetFeature.set("previewLabel", "드래그 이동");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
setGisEditPreviewCursor("grab");
|
||||
if (
|
||||
originalMoveGeometrySnapshot?.clone &&
|
||||
Array.isArray(lastTranslateCoordinate) &&
|
||||
Array.isArray(translateStartCoordinate)
|
||||
) {
|
||||
const geometry = originalMoveGeometrySnapshot.clone();
|
||||
const deltaX =
|
||||
Number(lastTranslateCoordinate[0] ?? 0) -
|
||||
Number(translateStartCoordinate[0] ?? 0);
|
||||
const deltaY =
|
||||
Number(lastTranslateCoordinate[1] ?? 0) -
|
||||
Number(translateStartCoordinate[1] ?? 0);
|
||||
geometry.translate(deltaX, deltaY);
|
||||
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
|
||||
const movedGripCoordinate = Array.isArray(moveGripCoordinate)
|
||||
? [
|
||||
Number(moveGripCoordinate[0] ?? 0) + deltaX,
|
||||
Number(moveGripCoordinate[1] ?? 0) + deltaY,
|
||||
]
|
||||
: lastTranslateCoordinate;
|
||||
applyGeometryMoveSnap(
|
||||
geometry,
|
||||
pipeSnapSource,
|
||||
map,
|
||||
movedGripCoordinate,
|
||||
28,
|
||||
);
|
||||
}
|
||||
targetFeature.setGeometry(geometry);
|
||||
}
|
||||
editUpdateChangeGeometry(
|
||||
editPendingInteraction!.changeId,
|
||||
writeGeometryObject(),
|
||||
`${layerName} ${objectId} 객체 위치를 지도에서 이동했습니다.`,
|
||||
{ keepMoveToolActive: true },
|
||||
);
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
});
|
||||
map.addInteraction(translate);
|
||||
gisEditTranslateRef.current = translate;
|
||||
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
|
||||
const snap = new Snap({
|
||||
source: pipeSnapSource,
|
||||
edge: true,
|
||||
vertex: true,
|
||||
pixelTolerance: 14,
|
||||
});
|
||||
map.addInteraction(snap);
|
||||
gisEditSnapRef.current = snap;
|
||||
} else if (editIsSnapEnabled && snapFeatures.length > 0) {
|
||||
const snap = new Snap({
|
||||
features: new Collection(snapFeatures),
|
||||
edge: true,
|
||||
vertex: true,
|
||||
pixelTolerance: 14,
|
||||
});
|
||||
map.addInteraction(snap);
|
||||
gisEditSnapRef.current = snap;
|
||||
}
|
||||
setGisEditPreviewCursor("grab");
|
||||
map.renderSync?.();
|
||||
}
|
||||
|
||||
if (editPendingInteraction.editType === "VERTEX_EDIT") {
|
||||
const geometry = targetFeature.getGeometry?.();
|
||||
const geometryType = geometry?.getType?.();
|
||||
const objectId = String(targetFeature.get("objectId") ?? "");
|
||||
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
|
||||
|
||||
if (
|
||||
!geometry ||
|
||||
!geometryType ||
|
||||
geometryType === "Point" ||
|
||||
geometryType === "MultiPoint"
|
||||
) {
|
||||
editAnnounceStatus(
|
||||
`${layerName} ${objectId} 객체는 꼭지점편집 대상 geometry가 아닙니다.`,
|
||||
);
|
||||
editClearPendingInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexSource = ensureGisEditVertexLayer();
|
||||
const vertexLayer = gisEditVertexLayerRef.current;
|
||||
if (!vertexSource || !vertexLayer) {
|
||||
editAnnounceStatus("꼭지점 편집 레이어를 준비하지 못했습니다.");
|
||||
editClearPendingInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
const renderVertexHandles = (
|
||||
hoveredMidpoint: VertexHandleCandidate | null = null,
|
||||
) => {
|
||||
vertexSource.clear();
|
||||
buildVertexHandleFeatures(targetFeature.getGeometry?.(), map).forEach(
|
||||
(handleFeature: Feature) => {
|
||||
vertexSource.addFeature(handleFeature);
|
||||
},
|
||||
);
|
||||
if (hoveredMidpoint) {
|
||||
vertexSource.addFeature(
|
||||
createVertexHandleFeature(
|
||||
hoveredMidpoint.coordinate,
|
||||
hoveredMidpoint.meta,
|
||||
),
|
||||
);
|
||||
}
|
||||
vertexSource.changed?.();
|
||||
applySelectedVertexHandleStyle();
|
||||
};
|
||||
|
||||
let geometryBeforeModifyKey = geometryObjectToKey(writeGeometryObject());
|
||||
let activeMidpointKey = "";
|
||||
let isDraggingHandle = false;
|
||||
let originalGeometrySnapshot =
|
||||
targetFeature.getGeometry?.()?.clone?.() ?? null;
|
||||
renderVertexHandles();
|
||||
targetFeature.set("previewLabel", "꼭지점 편집");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
setGisEditPreviewCursor("crosshair");
|
||||
|
||||
const pointerMoveKey = map.on("pointermove", (event: any) => {
|
||||
if (isDraggingHandle || event?.dragging) return;
|
||||
|
||||
const hoveredMidpoint = findHoveredMidpointCandidate(
|
||||
map,
|
||||
targetFeature.getGeometry?.(),
|
||||
event.coordinate,
|
||||
);
|
||||
const nextMidpointKey = hoveredMidpoint
|
||||
? `${hoveredMidpoint.meta.geometryType}:${hoveredMidpoint.meta.partIndex ?? -1}:${hoveredMidpoint.meta.polygonIndex ?? -1}:${hoveredMidpoint.meta.ringIndex ?? -1}:${hoveredMidpoint.meta.coordIndex}`
|
||||
: "";
|
||||
|
||||
if (nextMidpointKey === activeMidpointKey) return;
|
||||
|
||||
activeMidpointKey = nextMidpointKey;
|
||||
renderVertexHandles(hoveredMidpoint);
|
||||
setGisEditPreviewCursor(hoveredMidpoint ? "copy" : "crosshair");
|
||||
map.renderSync?.();
|
||||
});
|
||||
|
||||
const vertexTranslate = new Translate({
|
||||
layers: [vertexLayer],
|
||||
hitTolerance: 14,
|
||||
});
|
||||
|
||||
vertexTranslate.on("translatestart", (event: any) => {
|
||||
isDraggingHandle = true;
|
||||
geometryBeforeModifyKey = geometryObjectToKey(writeGeometryObject());
|
||||
originalGeometrySnapshot =
|
||||
targetFeature.getGeometry?.()?.clone?.() ?? null;
|
||||
clearSelectedVertexHandle();
|
||||
const handleFeature = event.features?.item?.(0);
|
||||
if (handleFeature) {
|
||||
handleFeature.setStyle(GIS_EDIT_VERTEX_DRAGGING_STYLE);
|
||||
}
|
||||
targetFeature.set("previewLabel", "꼭지점 편집 중");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
setGisEditPreviewCursor("grabbing");
|
||||
map.renderSync?.();
|
||||
});
|
||||
|
||||
vertexTranslate.on("translating", (event: any) => {
|
||||
const handleFeature = event.features?.item?.(0);
|
||||
const handleGeometry = handleFeature?.getGeometry?.();
|
||||
const nextCoordinate = handleGeometry?.getCoordinates?.();
|
||||
const handleMeta = handleFeature?.get?.("handleMeta") as
|
||||
| VertexHandleMeta
|
||||
| undefined;
|
||||
const handleMetaList =
|
||||
(handleFeature?.get?.("handleMetaList") as
|
||||
| VertexHandleMeta[]
|
||||
| undefined) ?? (handleMeta ? [handleMeta] : []);
|
||||
|
||||
if (
|
||||
handleMetaList.length === 0 ||
|
||||
!Array.isArray(nextCoordinate) ||
|
||||
!originalGeometrySnapshot?.clone
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetFeature.setGeometry(originalGeometrySnapshot.clone());
|
||||
handleMetaList.forEach((meta) => {
|
||||
applyVertexHandleMove(
|
||||
targetFeature.getGeometry?.(),
|
||||
meta,
|
||||
nextCoordinate,
|
||||
);
|
||||
});
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
});
|
||||
|
||||
vertexTranslate.on("translateend", (event: any) => {
|
||||
isDraggingHandle = false;
|
||||
const handleFeature = event.features?.item?.(0);
|
||||
const handleGeometry = handleFeature?.getGeometry?.();
|
||||
const nextCoordinate = handleGeometry?.getCoordinates?.();
|
||||
const handleMeta = handleFeature?.get?.("handleMeta") as
|
||||
| VertexHandleMeta
|
||||
| undefined;
|
||||
const handleMetaList =
|
||||
(handleFeature?.get?.("handleMetaList") as
|
||||
| VertexHandleMeta[]
|
||||
| undefined) ?? (handleMeta ? [handleMeta] : []);
|
||||
|
||||
targetFeature.set("previewLabel", "꼭지점 편집");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
setGisEditPreviewCursor("crosshair");
|
||||
activeMidpointKey = "";
|
||||
|
||||
if (handleMetaList.length === 0 || !Array.isArray(nextCoordinate)) {
|
||||
renderVertexHandles();
|
||||
map.renderSync?.();
|
||||
return;
|
||||
}
|
||||
|
||||
handleMetaList.forEach((meta) => {
|
||||
applyVertexHandleMove(
|
||||
targetFeature.getGeometry?.(),
|
||||
meta,
|
||||
nextCoordinate,
|
||||
);
|
||||
});
|
||||
const nextGeometry = writeGeometryObject();
|
||||
const geometryChanged =
|
||||
geometryBeforeModifyKey !== geometryObjectToKey(nextGeometry);
|
||||
renderVertexHandles();
|
||||
if (!geometryChanged) {
|
||||
editAnnounceStatus(
|
||||
`${layerName} ${objectId} 객체는 아직 형상이 바뀌지 않았습니다. 주황색 꼭지점 또는 가상 꼭지점을 직접 드래그해 주세요.`,
|
||||
);
|
||||
map.renderSync?.();
|
||||
return;
|
||||
}
|
||||
|
||||
editUpdateChangeGeometry(
|
||||
editPendingInteraction!.changeId,
|
||||
nextGeometry,
|
||||
`${layerName} ${objectId} 객체 꼭지점을 지도에서 조정했습니다.`,
|
||||
{ keepVertexEditActive: true },
|
||||
);
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
});
|
||||
|
||||
map.addInteraction(vertexTranslate);
|
||||
gisEditModifyRef.current = vertexTranslate;
|
||||
if (pipeSnapSource && pipeSnapSourceHasFeatures) {
|
||||
const snap = new Snap({
|
||||
source: pipeSnapSource,
|
||||
edge: true,
|
||||
vertex: true,
|
||||
pixelTolerance: 14,
|
||||
});
|
||||
map.addInteraction(snap);
|
||||
gisEditSnapRef.current = snap;
|
||||
} else if (editIsSnapEnabled && snapFeatures.length > 0) {
|
||||
const snap = new Snap({
|
||||
features: new Collection(snapFeatures),
|
||||
edge: true,
|
||||
vertex: true,
|
||||
pixelTolerance: 14,
|
||||
});
|
||||
map.addInteraction(snap);
|
||||
gisEditSnapRef.current = snap;
|
||||
}
|
||||
gisMaps.current?.renderSync?.();
|
||||
cleanupPointerMove = () => {
|
||||
unByKey(pointerMoveKey);
|
||||
};
|
||||
}
|
||||
|
||||
return cleanupPointerMove;
|
||||
}, [
|
||||
applySelectedVertexHandleStyle,
|
||||
clearGisEditVertexLayer,
|
||||
clearSelectedVertexHandle,
|
||||
collectGisEditSnapFeatures,
|
||||
ensureGisEditPipeSnapSource,
|
||||
ensureGisEditVertexLayer,
|
||||
ensureGisEditPreviewLayer,
|
||||
editIsSnapEnabled,
|
||||
editPendingInteraction,
|
||||
editSelectedLayer,
|
||||
editUpdateChangeGeometry,
|
||||
editAnnounceStatus,
|
||||
editClearPendingInteraction,
|
||||
editIsEditMode,
|
||||
mapReady,
|
||||
removeGisEditInteractions,
|
||||
shouldUsePipeSnapSource,
|
||||
gisMaps,
|
||||
gisEditMoveGripMetaRef,
|
||||
setGisEditPreviewCursor,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!mapReady ||
|
||||
!gisMaps.current ||
|
||||
!editIsEditMode ||
|
||||
editPendingInteraction?.editType !== "VERTEX_EDIT"
|
||||
) {
|
||||
clearSelectedVertexHandle();
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingInteraction = editPendingInteraction;
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Delete") return;
|
||||
if (
|
||||
isTypingElement(event.target) ||
|
||||
isTypingElement(document.activeElement)
|
||||
)
|
||||
return;
|
||||
|
||||
const source = ensureGisEditPreviewLayer();
|
||||
const map = gisMaps.current;
|
||||
if (!source || !map) return;
|
||||
|
||||
const targetFeature = source
|
||||
.getFeatures()
|
||||
.find(
|
||||
(feature: any) =>
|
||||
feature.get("changeId") === editPendingInteraction?.changeId,
|
||||
);
|
||||
if (!targetFeature) return;
|
||||
|
||||
const selectedMetaList = gisEditSelectedVertexMetaListRef.current ?? [];
|
||||
if (selectedMetaList.length === 0) {
|
||||
editAnnounceStatus(
|
||||
"Delete로 삭제할 주황색 실꼭지점을 먼저 클릭하세요.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteResult = deleteSelectedVertexHandles(
|
||||
targetFeature.getGeometry?.(),
|
||||
selectedMetaList,
|
||||
);
|
||||
if (!deleteResult.ok) {
|
||||
editAnnounceStatus(
|
||||
deleteResult.message ?? "꼭지점을 삭제하지 못했습니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
clearSelectedVertexHandle();
|
||||
|
||||
const objectId = String(targetFeature.get("objectId") ?? "");
|
||||
const layerName = editSelectedLayer?.layerName ?? "선택 레이어";
|
||||
const geometry = targetFeature.getGeometry();
|
||||
if (!geometry) {
|
||||
editAnnounceStatus("꼭지점 삭제 후 geometry를 확인하지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
targetFeature.set("previewLabel", "꼭지점 편집");
|
||||
targetFeature.setStyle(createGisEditPreviewStyle(targetFeature));
|
||||
editUpdateChangeGeometry(
|
||||
pendingInteraction.changeId,
|
||||
new GeoJSON().writeGeometryObject(geometry, {
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
dataProjection: FLOW_DIRECTION_SRS,
|
||||
}) as Record<string, unknown>,
|
||||
`${layerName} ${objectId} 객체 꼭지점을 삭제했습니다.`,
|
||||
{ keepVertexEditActive: true },
|
||||
);
|
||||
targetFeature.changed?.();
|
||||
source.changed?.();
|
||||
map.renderSync?.();
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
clearSelectedVertexHandle,
|
||||
ensureGisEditPreviewLayer,
|
||||
editIsEditMode,
|
||||
editPendingInteraction,
|
||||
editSelectedLayer,
|
||||
editUpdateChangeGeometry,
|
||||
editAnnounceStatus,
|
||||
mapReady,
|
||||
gisMaps,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!editIsEditMode ||
|
||||
(editActiveTool !== "MOVE" && editActiveTool !== "VERTEX_EDIT")
|
||||
) {
|
||||
removeGisEditInteractions();
|
||||
}
|
||||
}, [editActiveTool, editIsEditMode, removeGisEditInteractions]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
removeGisEditInteractions();
|
||||
if (gisEditVertexLayerRef.current)
|
||||
gisMaps.current?.removeLayer?.(gisEditVertexLayerRef.current);
|
||||
gisEditVertexLayerRef.current = null;
|
||||
gisEditVertexSourceRef.current = null;
|
||||
},
|
||||
[gisMaps, removeGisEditInteractions],
|
||||
);
|
||||
return {
|
||||
gisEditSelectedVertexHandleKeyRef,
|
||||
gisEditSelectedVertexMetaListRef,
|
||||
gisEditVertexLayerRef,
|
||||
clearGisEditVertexLayer,
|
||||
applySelectedVertexHandleStyle,
|
||||
clearSelectedVertexHandle,
|
||||
removeGisEditInteractions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
type MutableRefObject,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import { FLOOD_DNGR_MAP_LAYER_TYPE } from "../components/flood-dngr-map/flood-dngr-map-types";
|
||||
|
||||
type MapRuntimeBridgeOptions = {
|
||||
/** 공통 Gis가 생성한 지도 참조. 생성/파괴 소유권은 Gis에 유지한다. */
|
||||
gisMaps: MutableRefObject<any>;
|
||||
setMapReady: Dispatch<SetStateAction<boolean>>;
|
||||
loadFloodPredictionGridLayer: (
|
||||
sggCd: string,
|
||||
analysisDateTime: string,
|
||||
extent: number[],
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
/** 지도 준비 감지와 정보검색/GIS 업로드 메시지 연결을 소유한다.
|
||||
* 기존 300ms 준비 polling 및 callback 변경 시 구독 갱신/정리 순서를 보존한다.
|
||||
* 지도 객체 자체를 별도로 만들거나 전역 store에 저장하지 않는다.
|
||||
*/
|
||||
export function useMapRuntimeBridge({
|
||||
gisMaps,
|
||||
setMapReady,
|
||||
loadFloodPredictionGridLayer,
|
||||
}: MapRuntimeBridgeOptions) {
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
if (gisMaps.current) {
|
||||
setMapReady(true);
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handlePopupMessage = async (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
const data = event.data;
|
||||
const core = GisApp.Module.layer as any;
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE) {
|
||||
const layerNm = data.payload?.layerNm;
|
||||
const fidsRaw = data.payload?.fids;
|
||||
if (!layerNm || !fidsRaw || !core?.showMultipleSearchResultsFromGeoJSON)
|
||||
return;
|
||||
|
||||
let fids: string[] = [];
|
||||
try {
|
||||
fids = JSON.parse(fidsRaw);
|
||||
} catch (error) {
|
||||
console.error("다중 시설 메시지 파싱 실패:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FIT_EXTENT) {
|
||||
const extent = Array.isArray(data.payload?.extent)
|
||||
? data.payload.extent.map(Number)
|
||||
: [];
|
||||
const map = gisMaps.current;
|
||||
if (
|
||||
extent.length !== 4 ||
|
||||
extent.some((value: number) => !Number.isFinite(value)) ||
|
||||
!map?.getView
|
||||
) {
|
||||
return;
|
||||
}
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
data?.type === WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_FLOOD_GROUP
|
||||
) {
|
||||
const extent = Array.isArray(data.payload?.extent)
|
||||
? data.payload.extent.map(Number)
|
||||
: [];
|
||||
const sggCd = String(data.payload?.sggCd ?? "").trim();
|
||||
const frequency = String(data.payload?.sbmrgFrqExpln ?? "").trim();
|
||||
const analysisDateTime = String(
|
||||
data.payload?.analysisDateTime ?? "",
|
||||
).trim();
|
||||
const map = gisMaps.current;
|
||||
const select = GisApp.Module.select as any;
|
||||
if (
|
||||
extent.length !== 4 ||
|
||||
extent.some((value: number) => !Number.isFinite(value)) ||
|
||||
!/^\d{5}$/.test(sggCd) ||
|
||||
!/^(030|050|080|100)$/.test(frequency) ||
|
||||
!map?.getView ||
|
||||
!select?.selectFocusedBoundaryFeature
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await select.resetSelectedFeature?.();
|
||||
const selectedFeatureIds = new Set<string>();
|
||||
let floodLayerVisible = false;
|
||||
const selectLoadedGroupFeatures = (source: any) => {
|
||||
const features = source?.getFeatures?.() ?? [];
|
||||
features.forEach((feature: any) => {
|
||||
const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim();
|
||||
const featureFrequency = String(
|
||||
feature.get?.("sbmrg_frq_expln") ?? "",
|
||||
).trim();
|
||||
if (featureSggCd !== sggCd || featureFrequency !== frequency)
|
||||
return;
|
||||
const featureId = String(
|
||||
feature.getId?.() ?? feature.get?.("fid") ?? "",
|
||||
);
|
||||
if (featureId && selectedFeatureIds.has(featureId)) return;
|
||||
if (featureId) selectedFeatureIds.add(featureId);
|
||||
select.selectFocusedBoundaryFeature(feature, true);
|
||||
});
|
||||
};
|
||||
|
||||
map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer: any) => {
|
||||
if (
|
||||
String(layer.get?.("lyrTyNm") ?? "").trim() !==
|
||||
FLOOD_DNGR_MAP_LAYER_TYPE
|
||||
)
|
||||
return;
|
||||
if (layer.getVisible?.() !== false) floodLayerVisible = true;
|
||||
const source = layer.getSource?.();
|
||||
if (!source?.getFeatures) return;
|
||||
selectLoadedGroupFeatures(source);
|
||||
source.once?.("featuresloadend", () =>
|
||||
selectLoadedGroupFeatures(source),
|
||||
);
|
||||
});
|
||||
if (!floodLayerVisible) {
|
||||
(event.source as Window | null)?.postMessage?.(
|
||||
{ type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED },
|
||||
event.origin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
});
|
||||
void loadFloodPredictionGridLayer(sggCd, analysisDateTime, extent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.GIS_UPLOAD_COMPLETED) {
|
||||
const rawLayerTypeNames = data.payload?.refreshLayerTypeNames;
|
||||
let layerTypeNames = ["tb_ms_swage_ppl"];
|
||||
if (rawLayerTypeNames) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawLayerTypeNames);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
layerTypeNames = parsed
|
||||
.map((item) => String(item ?? "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("GIS 업로드 레이어 refresh 메시지 파싱 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTargetNames = new Set(layerTypeNames);
|
||||
gisMaps.current?.getAllLayers?.().forEach((layer: any) => {
|
||||
const layerTypeName = String(layer?.get?.("lyrTyNm") ?? "").trim();
|
||||
if (!refreshTargetNames.has(layerTypeName)) return;
|
||||
|
||||
const source = layer.getSource?.();
|
||||
source?.updateParams?.({ _t: Date.now() });
|
||||
source?.refresh?.();
|
||||
source?.changed?.();
|
||||
layer.changed?.();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handlePopupMessage);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("message", handlePopupMessage);
|
||||
};
|
||||
}, [gisMaps, setMapReady, loadFloodPredictionGridLayer]);
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
"use client";
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import type { SinkholeScope } from "../components/sinkhole/sinkhole-types";
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import CircleGeom from "ol/geom/Circle.js";
|
||||
import Point from "ol/geom/Point.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import {
|
||||
buildSinkholeListQueryString,
|
||||
normalizeSinkholeFilters,
|
||||
SINKHOLE_LAYER_ID,
|
||||
SINKHOLE_LAYER_NAME,
|
||||
SINKHOLE_LAYER_TYPE,
|
||||
SINKHOLE_SELECTED_LAYER_ID,
|
||||
type SinkholeDetail,
|
||||
type SinkholeFilters,
|
||||
type SinkholeListItem,
|
||||
} from "../components/sinkhole/sinkhole-types";
|
||||
import { SINKHOLE_DEFAULT_POINT_LIMIT } from "../model/map-constants";
|
||||
import {
|
||||
buildSinkholeFeatureCollectionFromItems,
|
||||
resolveSinkholePointVariant,
|
||||
createSinkholePointStyle,
|
||||
createSinkholeRadiusStyle,
|
||||
createSinkholeRadiusLabelStyle,
|
||||
} from "./sinkhole-features";
|
||||
|
||||
/** 지반침하 사고·반경·주변관로 도형의 조회와 표시를 소유한다. */
|
||||
type Options = {
|
||||
gisMaps: React.RefObject<any>;
|
||||
ctpv: SelectDataType[];
|
||||
sgg: SelectDataType[];
|
||||
appliedSinkholeFilters: SinkholeFilters;
|
||||
mapReady: boolean;
|
||||
isSinkholeChecked: boolean;
|
||||
sinkholeScope: SinkholeScope;
|
||||
sinkholeSelectionVersion: number;
|
||||
selectedSinkholeDetail: SinkholeDetail | null;
|
||||
sinkholeRadiusMeters: number;
|
||||
};
|
||||
|
||||
/** 지반침하 사고·반경·주변관로 도형의 조회와 표시를 소유한다. */
|
||||
export function useSinkholeLayers({
|
||||
gisMaps,
|
||||
ctpv,
|
||||
sgg,
|
||||
appliedSinkholeFilters,
|
||||
mapReady,
|
||||
isSinkholeChecked,
|
||||
sinkholeScope,
|
||||
sinkholeSelectionVersion,
|
||||
selectedSinkholeDetail,
|
||||
sinkholeRadiusMeters,
|
||||
}: Options) {
|
||||
const sinkholeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
// 검색 변경·레이어 해제 후 늦은 응답이 지도를 덮어쓰지 않도록 요청 수명을 보관한다.
|
||||
const requestSequence = useRef(0);
|
||||
const requestController = useRef<AbortController | null>(null);
|
||||
|
||||
const sinkholeSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const sinkholeSelectedLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const sinkholeSelectedSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const sinkholeFocusedPipeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const sinkholeFocusedPipeSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const removeSinkholeLayer = useCallback(() => {
|
||||
requestSequence.current += 1;
|
||||
requestController.current?.abort();
|
||||
requestController.current = null;
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(SINKHOLE_LAYER_ID);
|
||||
sinkholeLayerRef.current = null;
|
||||
sinkholeSourceRef.current = null;
|
||||
}, []);
|
||||
|
||||
const ensureSinkholeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (sinkholeLayerRef.current && sinkholeSourceRef.current) {
|
||||
return sinkholeSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9992,
|
||||
updateWhileAnimating: false,
|
||||
updateWhileInteracting: false,
|
||||
});
|
||||
|
||||
layer.set("id", SINKHOLE_LAYER_ID);
|
||||
layer.set("lyrNm", SINKHOLE_LAYER_NAME);
|
||||
layer.set("lyrTyNm", SINKHOLE_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "Y");
|
||||
layer.set("chcYn", "N");
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(SINKHOLE_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
sinkholeLayerRef.current = layer;
|
||||
sinkholeSourceRef.current = source;
|
||||
return source;
|
||||
}, [gisMaps]);
|
||||
|
||||
const clearSinkholeSelectionLayer = useCallback(() => {
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(SINKHOLE_SELECTED_LAYER_ID);
|
||||
sinkholeSelectedLayerRef.current = null;
|
||||
sinkholeSelectedSourceRef.current = null;
|
||||
}, []);
|
||||
|
||||
const clearSinkholeFocusedPipeLayer = useCallback(() => {
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.("SN_SINKHOLE_FOCUSED_PIPE_LAYER");
|
||||
sinkholeFocusedPipeLayerRef.current = null;
|
||||
sinkholeFocusedPipeSourceRef.current = null;
|
||||
}, []);
|
||||
|
||||
const ensureSinkholeSelectionLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (sinkholeSelectedLayerRef.current && sinkholeSelectedSourceRef.current) {
|
||||
return sinkholeSelectedSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9996,
|
||||
updateWhileAnimating: false,
|
||||
updateWhileInteracting: false,
|
||||
});
|
||||
|
||||
layer.set("id", SINKHOLE_SELECTED_LAYER_ID);
|
||||
layer.set("lyrNm", `${SINKHOLE_LAYER_NAME}-선택`);
|
||||
layer.set("lyrTyNm", SINKHOLE_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(SINKHOLE_SELECTED_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
sinkholeSelectedLayerRef.current = layer;
|
||||
sinkholeSelectedSourceRef.current = source;
|
||||
return source;
|
||||
}, [gisMaps]);
|
||||
|
||||
const ensureSinkholeFocusedPipeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (
|
||||
sinkholeFocusedPipeLayerRef.current &&
|
||||
sinkholeFocusedPipeSourceRef.current
|
||||
) {
|
||||
return sinkholeFocusedPipeSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9999,
|
||||
updateWhileAnimating: false,
|
||||
updateWhileInteracting: false,
|
||||
});
|
||||
|
||||
layer.set("id", "SN_SINKHOLE_FOCUSED_PIPE_LAYER");
|
||||
layer.set("lyrNm", "지반침하-관로강조");
|
||||
layer.set("lyrTyNm", "tb_ms_swage_ppl");
|
||||
layer.set("clickPsblYn", "N");
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.("SN_SINKHOLE_FOCUSED_PIPE_LAYER");
|
||||
map.addLayer(layer);
|
||||
sinkholeFocusedPipeLayerRef.current = layer;
|
||||
sinkholeFocusedPipeSourceRef.current = source;
|
||||
return source;
|
||||
}, [gisMaps]);
|
||||
|
||||
const renderSelectedSinkhole = useCallback(
|
||||
(detail: SinkholeDetail | null, radiusMeters: number) => {
|
||||
if (!detail || detail.x == null || detail.y == null) {
|
||||
clearSinkholeSelectionLayer();
|
||||
return;
|
||||
}
|
||||
|
||||
const source = ensureSinkholeSelectionLayer();
|
||||
if (!source) return;
|
||||
source.clear();
|
||||
|
||||
const pointFeature = new Feature({
|
||||
geometry: new Point([detail.x, detail.y]),
|
||||
});
|
||||
const pointVariant = resolveSinkholePointVariant(detail.ocrnYmd);
|
||||
pointFeature.setStyle(createSinkholePointStyle(pointVariant, true));
|
||||
|
||||
const radiusFeature = new Feature({
|
||||
geometry: new CircleGeom([detail.x, detail.y], radiusMeters),
|
||||
});
|
||||
radiusFeature.setStyle(createSinkholeRadiusStyle(pointVariant));
|
||||
|
||||
// 원의 북쪽 경계에 라벨을 두고 같은 source에서 관리해 선택 해제 시 함께 정리한다.
|
||||
const radiusLabelFeature = new Feature({
|
||||
geometry: new Point([detail.x, detail.y + radiusMeters]),
|
||||
});
|
||||
radiusLabelFeature.set("sinkholeRadiusLabel", true);
|
||||
radiusLabelFeature.setStyle(
|
||||
createSinkholeRadiusLabelStyle(radiusMeters, pointVariant),
|
||||
);
|
||||
|
||||
source.addFeatures([radiusFeature, radiusLabelFeature, pointFeature]);
|
||||
},
|
||||
[clearSinkholeSelectionLayer, ensureSinkholeSelectionLayer],
|
||||
);
|
||||
|
||||
const focusSinkholeOnMap = useCallback(
|
||||
(detail: SinkholeDetail, radiusMeters: number) => {
|
||||
if (!gisMaps.current || detail.x == null || detail.y == null) return;
|
||||
|
||||
const radiusGeometry = new CircleGeom([detail.x, detail.y], radiusMeters);
|
||||
gisMaps.current.getView?.().fit?.(radiusGeometry.getExtent(), {
|
||||
duration: 350,
|
||||
padding: [100, 380, 100, 120],
|
||||
maxZoom: 17,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
|
||||
const buildSinkholeQueryString = useCallback(
|
||||
(filters: SinkholeFilters, page = 0, size = SINKHOLE_DEFAULT_POINT_LIMIT) =>
|
||||
buildSinkholeListQueryString(filters, ctpv, sgg, page, size),
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadSinkholeLayer = useCallback(
|
||||
async (filters = appliedSinkholeFilters) => {
|
||||
if (!mapReady || !gisMaps.current || !isSinkholeChecked) return;
|
||||
|
||||
const source = ensureSinkholeLayer();
|
||||
if (!source) return;
|
||||
|
||||
const effectiveFilters = normalizeSinkholeFilters(filters, sinkholeScope);
|
||||
const sequence = ++requestSequence.current;
|
||||
requestController.current?.abort();
|
||||
const controller = new AbortController();
|
||||
requestController.current = controller;
|
||||
try {
|
||||
const params = new URLSearchParams(
|
||||
buildSinkholeQueryString(
|
||||
effectiveFilters,
|
||||
0,
|
||||
SINKHOLE_DEFAULT_POINT_LIMIT,
|
||||
),
|
||||
);
|
||||
const rawResponse = await getRawJson(
|
||||
`/sewerage-service/api/v1/sinkholes?${params.toString()}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (controller.signal.aborted || sequence !== requestSequence.current)
|
||||
return;
|
||||
const items = Array.isArray(rawResponse?.data?.items)
|
||||
? (rawResponse.data.items as SinkholeListItem[])
|
||||
: Array.isArray(rawResponse?.items)
|
||||
? (rawResponse.items as SinkholeListItem[])
|
||||
: [];
|
||||
const featureCollection =
|
||||
buildSinkholeFeatureCollectionFromItems(items);
|
||||
|
||||
if (!featureCollection) {
|
||||
console.warn(
|
||||
"지반침하 목록 응답을 feature collection으로 변환하지 못했습니다.",
|
||||
rawResponse,
|
||||
);
|
||||
source.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const features = new GeoJSON().readFeatures(featureCollection, {
|
||||
dataProjection: "EPSG:5179",
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
source.clear();
|
||||
features.forEach((feature: any) => {
|
||||
const variant = resolveSinkholePointVariant(
|
||||
String(feature?.get?.("ocrnYmd") ?? ""),
|
||||
);
|
||||
feature.setStyle(createSinkholePointStyle(variant, false));
|
||||
});
|
||||
source.addFeatures(features);
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || sequence !== requestSequence.current)
|
||||
return;
|
||||
console.error("지반침하 레이어 조회 실패:", error);
|
||||
source.clear();
|
||||
} finally {
|
||||
if (requestController.current === controller)
|
||||
requestController.current = null;
|
||||
}
|
||||
},
|
||||
[
|
||||
appliedSinkholeFilters,
|
||||
buildSinkholeQueryString,
|
||||
ensureSinkholeLayer,
|
||||
gisMaps,
|
||||
isSinkholeChecked,
|
||||
mapReady,
|
||||
sinkholeScope,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !isSinkholeChecked) {
|
||||
removeSinkholeLayer();
|
||||
return;
|
||||
}
|
||||
|
||||
void loadSinkholeLayer(appliedSinkholeFilters);
|
||||
}, [
|
||||
appliedSinkholeFilters,
|
||||
isSinkholeChecked,
|
||||
loadSinkholeLayer,
|
||||
mapReady,
|
||||
removeSinkholeLayer,
|
||||
sinkholeSelectionVersion,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
renderSelectedSinkhole(selectedSinkholeDetail, sinkholeRadiusMeters);
|
||||
}, [renderSelectedSinkhole, selectedSinkholeDetail, sinkholeRadiusMeters]);
|
||||
// 이 hook이 만든 레이어만 해제한다. 다른 지도 인스턴스의 공용 레이어를 지우지 않는다.
|
||||
useEffect(
|
||||
() => () => {
|
||||
requestSequence.current += 1;
|
||||
requestController.current?.abort();
|
||||
[
|
||||
sinkholeLayerRef,
|
||||
sinkholeSelectedLayerRef,
|
||||
sinkholeFocusedPipeLayerRef,
|
||||
].forEach((ref) => {
|
||||
if (ref.current) gisMaps.current?.removeLayer?.(ref.current);
|
||||
ref.current = null;
|
||||
});
|
||||
},
|
||||
[gisMaps],
|
||||
);
|
||||
return {
|
||||
removeSinkholeLayer,
|
||||
clearSinkholeSelectionLayer,
|
||||
clearSinkholeFocusedPipeLayer,
|
||||
ensureSinkholeFocusedPipeLayer,
|
||||
renderSelectedSinkhole,
|
||||
focusSinkholeOnMap,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
import VectorLayer from "ol/layer/Vector.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import {
|
||||
EMPTY_WATER_RISK_SESSION_SNAPSHOT,
|
||||
type WaterRiskMapView,
|
||||
type WaterRiskResultRow,
|
||||
type WaterRiskSessionSnapshot,
|
||||
} from "../components/water-risk/water-risk-types";
|
||||
import {
|
||||
WATER_RISK_AREA_LAYER_ID,
|
||||
WATER_RISK_PIPE_LAYER_ID,
|
||||
WATER_RISK_MANHOLE_LAYER_ID,
|
||||
} from "../model/map-constants";
|
||||
import {
|
||||
createWaterRiskAreaStyle,
|
||||
createWaterRiskPipeStyle,
|
||||
createWaterRiskManholeStyle,
|
||||
} from "./water-risk-styles";
|
||||
import type { MutableRefObject } from "react";
|
||||
|
||||
type Options = {
|
||||
gisMaps: MutableRefObject<any>;
|
||||
mapReady: boolean;
|
||||
onSelectResult: () => void;
|
||||
};
|
||||
|
||||
/** 통수위험 결과 레이어·정보 카드·팝업 수명을 소유한다. 부모 화면 선택 정리는 명령 callback으로 요청한다. */
|
||||
export function useWaterRiskLayer({
|
||||
gisMaps,
|
||||
mapReady,
|
||||
onSelectResult,
|
||||
}: Options) {
|
||||
const waterRiskPopupWindowRef = useRef<Window | null>(null);
|
||||
|
||||
const waterRiskPopupCloseWatcherRef = useRef<number | null>(null);
|
||||
|
||||
const waterRiskAreaLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const waterRiskAreaSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const waterRiskPipeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const waterRiskPipeSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const waterRiskManholeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
|
||||
const waterRiskManholeSourceRef = useRef<VectorSource | null>(null);
|
||||
|
||||
const [waterRiskSession, setWaterRiskSession] =
|
||||
useState<WaterRiskSessionSnapshot>(EMPTY_WATER_RISK_SESSION_SNAPSHOT);
|
||||
|
||||
// 팝업에서 실제 API로 조회한 결과 행과 GeoJSON을 지도·정보카드의 단일 원본으로 보관한다.
|
||||
const [selectedWaterRiskApiResult, setSelectedWaterRiskApiResult] =
|
||||
useState<WaterRiskResultRow | null>(null);
|
||||
|
||||
const [selectedWaterRiskMapView, setSelectedWaterRiskMapView] =
|
||||
useState<WaterRiskMapView | null>(null);
|
||||
|
||||
const [waterRiskInfoAnchor, setWaterRiskInfoAnchor] = useState<
|
||||
[number, number] | null
|
||||
>(null);
|
||||
|
||||
const [waterRiskInfoCardStyle, setWaterRiskInfoCardStyle] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
|
||||
const waterRiskSessionSnapshot = useMemo<WaterRiskSessionSnapshot>(
|
||||
() => waterRiskSession,
|
||||
[waterRiskSession],
|
||||
);
|
||||
|
||||
const syncWaterRiskPopup = useCallback(() => {
|
||||
const popup = waterRiskPopupWindowRef.current;
|
||||
if (!popup || popup.closed) {
|
||||
waterRiskPopupWindowRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
popup.postMessage(
|
||||
{
|
||||
type: WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SYNC,
|
||||
payload: waterRiskSessionSnapshot,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
}, [waterRiskSessionSnapshot]);
|
||||
|
||||
const selectedWaterRiskResultRow = useMemo(
|
||||
() => selectedWaterRiskApiResult,
|
||||
[selectedWaterRiskApiResult],
|
||||
);
|
||||
|
||||
// 지도카드는 실제 GIS geometry까지 결합된 mapView 위험상태를 목록 상태보다 우선한다.
|
||||
const selectedWaterRiskDisplayStatus = useMemo<
|
||||
WaterRiskResultRow["riskStatus"]
|
||||
>(
|
||||
() =>
|
||||
selectedWaterRiskMapView?.riskStatus ??
|
||||
selectedWaterRiskResultRow?.riskStatus ??
|
||||
"NORMAL",
|
||||
[
|
||||
selectedWaterRiskMapView?.riskStatus,
|
||||
selectedWaterRiskResultRow?.riskStatus,
|
||||
],
|
||||
);
|
||||
|
||||
/** 선택 영역 오른쪽에 중점관리지역 강우·침수 요약카드를 배치한다. */
|
||||
const syncWaterRiskInfoCardPosition = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map || !waterRiskInfoAnchor) {
|
||||
setWaterRiskInfoCardStyle(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const [mapWidth = 0, mapHeight = 0] = map.getSize?.() ?? [];
|
||||
const [pixelX = 0, pixelY = 0] =
|
||||
map.getPixelFromCoordinate?.(waterRiskInfoAnchor) ?? [];
|
||||
const cardWidth = 244;
|
||||
const cardHeight = 180;
|
||||
const left = pixelX + 22;
|
||||
const top = pixelY - cardHeight / 2;
|
||||
|
||||
if (
|
||||
pixelX < 0 ||
|
||||
pixelY < 0 ||
|
||||
pixelX > mapWidth ||
|
||||
pixelY > mapHeight ||
|
||||
left > mapWidth ||
|
||||
top > mapHeight ||
|
||||
left + cardWidth < 0 ||
|
||||
top + cardHeight < 0
|
||||
) {
|
||||
setWaterRiskInfoCardStyle(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setWaterRiskInfoCardStyle({ left, top });
|
||||
}, [waterRiskInfoAnchor]);
|
||||
|
||||
const clearWaterRiskPopupCloseWatcher = useCallback(() => {
|
||||
if (waterRiskPopupCloseWatcherRef.current !== null) {
|
||||
window.clearInterval(waterRiskPopupCloseWatcherRef.current);
|
||||
waterRiskPopupCloseWatcherRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskAreaLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (waterRiskAreaLayerRef.current && waterRiskAreaSourceRef.current) {
|
||||
const layers = map.getLayers?.().getArray?.() ?? [];
|
||||
if (!layers.includes(waterRiskAreaLayerRef.current)) {
|
||||
map.addLayer(waterRiskAreaLayerRef.current);
|
||||
}
|
||||
waterRiskAreaLayerRef.current.setZIndex?.(9996);
|
||||
return waterRiskAreaSourceRef.current;
|
||||
}
|
||||
|
||||
// 통수위험 overlay 초기화는 자신의 stale layer만 정리해 GIS 편집 preview를 보존한다.
|
||||
const layers = map.getLayers?.().getArray?.() ?? [];
|
||||
layers.forEach((existingLayer: any) => {
|
||||
if (existingLayer?.get?.("id") === WATER_RISK_AREA_LAYER_ID) {
|
||||
map.removeLayer(existingLayer);
|
||||
}
|
||||
});
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9996,
|
||||
});
|
||||
|
||||
layer.set("id", WATER_RISK_AREA_LAYER_ID);
|
||||
layer.set("lyrNm", "통수위험 중점관리지역");
|
||||
layer.set("lyrTyNm", WATER_RISK_AREA_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
waterRiskAreaLayerRef.current = layer;
|
||||
waterRiskAreaSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskPipeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (waterRiskPipeLayerRef.current && waterRiskPipeSourceRef.current) {
|
||||
return waterRiskPipeSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9998,
|
||||
});
|
||||
|
||||
layer.set("id", WATER_RISK_PIPE_LAYER_ID);
|
||||
layer.set("lyrNm", "통수위험 유출관로");
|
||||
layer.set("lyrTyNm", WATER_RISK_PIPE_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
waterRiskPipeLayerRef.current = layer;
|
||||
waterRiskPipeSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskManholeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (waterRiskManholeLayerRef.current && waterRiskManholeSourceRef.current) {
|
||||
return waterRiskManholeSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({ source, zIndex: 9999 });
|
||||
layer.set("id", WATER_RISK_MANHOLE_LAYER_ID);
|
||||
layer.set("lyrNm", "통수위험 맨홀 Flooding");
|
||||
layer.set("lyrTyNm", WATER_RISK_MANHOLE_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
waterRiskManholeLayerRef.current = layer;
|
||||
waterRiskManholeSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const clearWaterRiskLayers = useCallback(() => {
|
||||
waterRiskAreaSourceRef.current?.clear();
|
||||
waterRiskPipeSourceRef.current?.clear();
|
||||
waterRiskManholeSourceRef.current?.clear();
|
||||
}, []);
|
||||
|
||||
/** 통수위험 팝업 종료 시 지도 강조·범례·정보카드 선택상태를 모두 초기화한다. */
|
||||
const clearWaterRiskSelection = useCallback(() => {
|
||||
clearWaterRiskLayers();
|
||||
setWaterRiskSession(EMPTY_WATER_RISK_SESSION_SNAPSHOT);
|
||||
setSelectedWaterRiskApiResult(null);
|
||||
setSelectedWaterRiskMapView(null);
|
||||
setWaterRiskInfoAnchor(null);
|
||||
setWaterRiskInfoCardStyle(null);
|
||||
}, [clearWaterRiskLayers]);
|
||||
|
||||
const renderWaterRiskSelection = useCallback(
|
||||
(areaId: string | null, mapView: WaterRiskMapView | null) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
const areaSource = ensureWaterRiskAreaLayer();
|
||||
if (!areaSource) return;
|
||||
|
||||
areaSource.clear();
|
||||
const pipeSource = ensureWaterRiskPipeLayer();
|
||||
pipeSource?.clear();
|
||||
const manholeSource = ensureWaterRiskManholeLayer();
|
||||
manholeSource?.clear();
|
||||
|
||||
if (!areaId || !mapView || mapView.areaId !== areaId) return;
|
||||
|
||||
const geoJson = new GeoJSON();
|
||||
const projection = mapView.projection || "EPSG:5179";
|
||||
let focusGeometry: any | null = null;
|
||||
|
||||
if (mapView.areaGeometry) {
|
||||
const geometry = geoJson.readGeometry(mapView.areaGeometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const areaFeature = new Feature({
|
||||
geometry,
|
||||
areaId: mapView.areaId,
|
||||
areaName: mapView.areaName,
|
||||
riskStatus: mapView.riskStatus,
|
||||
});
|
||||
areaFeature.setStyle(createWaterRiskAreaStyle(areaFeature));
|
||||
areaSource.addFeature(areaFeature);
|
||||
focusGeometry = geometry;
|
||||
}
|
||||
|
||||
mapView.pipes.forEach((pipe) => {
|
||||
if (!pipeSource || !pipe.geometry) return;
|
||||
const geometry = geoJson.readGeometry(pipe.geometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const pipeFeature = new Feature({
|
||||
geometry,
|
||||
pipeId: pipe.pipeId,
|
||||
displayLabel: pipe.displayLabel || pipe.pipeId,
|
||||
capacityRatio: pipe.capacityRatio ?? 0,
|
||||
});
|
||||
pipeFeature.setStyle(createWaterRiskPipeStyle(pipeFeature));
|
||||
pipeSource.addFeature(pipeFeature);
|
||||
if (!focusGeometry) focusGeometry = geometry;
|
||||
});
|
||||
|
||||
mapView.manholes.forEach((manhole) => {
|
||||
if (!manholeSource || !manhole.geometry) return;
|
||||
const geometry = geoJson.readGeometry(manhole.geometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const manholeFeature = new Feature({
|
||||
geometry,
|
||||
manholeId: manhole.manholeId,
|
||||
displayLabel: manhole.displayLabel || manhole.manholeId,
|
||||
floodingFlow: manhole.floodingFlow ?? 0,
|
||||
});
|
||||
manholeFeature.setStyle(createWaterRiskManholeStyle(manholeFeature));
|
||||
manholeSource.addFeature(manholeFeature);
|
||||
if (!focusGeometry) focusGeometry = geometry;
|
||||
});
|
||||
|
||||
if (!focusGeometry) return;
|
||||
const extent = focusGeometry.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
setWaterRiskInfoAnchor([maxX, (minY + maxY) / 2]);
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [120, 120, 120, 120],
|
||||
maxZoom: 15,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
},
|
||||
[
|
||||
ensureWaterRiskAreaLayer,
|
||||
ensureWaterRiskManholeLayer,
|
||||
ensureWaterRiskPipeLayer,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
if (!waterRiskSession.selectedAreaId) {
|
||||
clearWaterRiskLayers();
|
||||
setWaterRiskInfoAnchor(null);
|
||||
setWaterRiskInfoCardStyle(null);
|
||||
return;
|
||||
}
|
||||
|
||||
renderWaterRiskSelection(
|
||||
waterRiskSession.selectedAreaId,
|
||||
selectedWaterRiskMapView,
|
||||
);
|
||||
}, [
|
||||
clearWaterRiskLayers,
|
||||
mapReady,
|
||||
renderWaterRiskSelection,
|
||||
selectedWaterRiskMapView,
|
||||
waterRiskSession.selectedAreaId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedWaterRiskMapView) setWaterRiskInfoAnchor(null);
|
||||
}, [selectedWaterRiskMapView]);
|
||||
|
||||
useEffect(() => {
|
||||
const popup = waterRiskPopupWindowRef.current;
|
||||
if (!popup || popup.closed) return;
|
||||
syncWaterRiskPopup();
|
||||
}, [syncWaterRiskPopup, waterRiskSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current || !waterRiskInfoAnchor) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const syncDuringMapRender = () => syncWaterRiskInfoCardPosition();
|
||||
syncWaterRiskInfoCardPosition();
|
||||
map.on?.("postrender", syncDuringMapRender);
|
||||
map.on?.("moveend", syncDuringMapRender);
|
||||
|
||||
return () => {
|
||||
map.un?.("postrender", syncDuringMapRender);
|
||||
map.un?.("moveend", syncDuringMapRender);
|
||||
};
|
||||
}, [mapReady, syncWaterRiskInfoCardPosition, waterRiskInfoAnchor]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWaterRiskPopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
const data = event.data;
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_READY) {
|
||||
syncWaterRiskPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT) {
|
||||
const areaId = String(data.payload?.areaId ?? "").trim();
|
||||
if (!areaId) return;
|
||||
const resultRow = data.payload?.resultRow as
|
||||
| WaterRiskResultRow
|
||||
| undefined;
|
||||
const mapView = data.payload?.mapView as WaterRiskMapView | undefined;
|
||||
if (!resultRow || !mapView || mapView.areaId !== areaId) return;
|
||||
setSelectedWaterRiskApiResult(resultRow);
|
||||
setSelectedWaterRiskMapView(mapView);
|
||||
setWaterRiskSession((current) => ({
|
||||
...current,
|
||||
selectedAreaId: areaId,
|
||||
}));
|
||||
onSelectResult();
|
||||
return;
|
||||
}
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_CLOSED) {
|
||||
clearWaterRiskPopupCloseWatcher();
|
||||
waterRiskPopupWindowRef.current = null;
|
||||
clearWaterRiskSelection();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleWaterRiskPopupMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleWaterRiskPopupMessage);
|
||||
};
|
||||
}, [
|
||||
clearWaterRiskPopupCloseWatcher,
|
||||
clearWaterRiskSelection,
|
||||
onSelectResult,
|
||||
syncWaterRiskPopup,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearWaterRiskPopupCloseWatcher();
|
||||
const popup = waterRiskPopupWindowRef.current;
|
||||
if (popup && !popup.closed) {
|
||||
popup.close();
|
||||
}
|
||||
waterRiskPopupWindowRef.current = null;
|
||||
};
|
||||
}, [clearWaterRiskPopupCloseWatcher]);
|
||||
const watchPopup = useCallback(
|
||||
(popup: Window) => {
|
||||
clearWaterRiskPopupCloseWatcher();
|
||||
waterRiskPopupWindowRef.current = popup;
|
||||
waterRiskPopupCloseWatcherRef.current = window.setInterval(() => {
|
||||
if (!popup.closed) return;
|
||||
clearWaterRiskPopupCloseWatcher();
|
||||
waterRiskPopupWindowRef.current = null;
|
||||
clearWaterRiskSelection();
|
||||
}, 300);
|
||||
},
|
||||
[clearWaterRiskPopupCloseWatcher, clearWaterRiskSelection],
|
||||
);
|
||||
useEffect(() => () => clearWaterRiskLayers(), [clearWaterRiskLayers]);
|
||||
return {
|
||||
selectedWaterRiskResultRow,
|
||||
waterRiskInfoCardStyle,
|
||||
selectedWaterRiskMapView,
|
||||
selectedWaterRiskDisplayStatus,
|
||||
watchPopup,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/** 통수위험 결과의 도형별 스타일. 기존 값과 처리 순서를 유지한다. */
|
||||
import Fill from "ol/style/Fill.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Text from "ol/style/Text.js";
|
||||
import { buildWaterRiskStatusLabel } from "@/app/(map-layout)/map/components/water-risk/water-risk-types";
|
||||
|
||||
export const buildWaterRiskStatusFillColor = (
|
||||
riskStatus: "DANGER" | "NORMAL",
|
||||
) =>
|
||||
riskStatus === "DANGER"
|
||||
? "rgba(198, 40, 40, 0.24)"
|
||||
: "rgba(21, 101, 192, 0.2)";
|
||||
|
||||
export const buildWaterRiskStatusStrokeColor = (
|
||||
riskStatus: "DANGER" | "NORMAL",
|
||||
) =>
|
||||
riskStatus === "DANGER"
|
||||
? "rgba(198, 40, 40, 0.92)"
|
||||
: "rgba(21, 101, 192, 0.92)";
|
||||
|
||||
/** capacity를 첨부 범례의 다섯 고정 구간색으로 변환한다. */
|
||||
export const buildWaterRiskCapacityColor = (capacityRatio: number) => {
|
||||
if (capacityRatio <= 0.25) return "#0000ff";
|
||||
if (capacityRatio <= 0.5) return "#00d9ff";
|
||||
if (capacityRatio <= 0.75) return "#00d95f";
|
||||
if (capacityRatio < 1) return "#ffe100";
|
||||
return "#ff1c15";
|
||||
};
|
||||
|
||||
export const createWaterRiskAreaStyle = (feature: any) => {
|
||||
const areaName = String(feature.get("areaName") ?? "");
|
||||
const riskStatus = String(feature.get("riskStatus") ?? "NORMAL") as
|
||||
| "DANGER"
|
||||
| "NORMAL";
|
||||
return new Style({
|
||||
fill: new Fill({ color: buildWaterRiskStatusFillColor(riskStatus) }),
|
||||
stroke: new Stroke({
|
||||
color: buildWaterRiskStatusStrokeColor(riskStatus),
|
||||
width: 3,
|
||||
}),
|
||||
text: new Text({
|
||||
text: `${areaName}\n${buildWaterRiskStatusLabel(riskStatus)}`,
|
||||
font: "700 13px sans-serif",
|
||||
fill: new Fill({ color: "#ffffff" }),
|
||||
backgroundFill: new Fill({
|
||||
color: buildWaterRiskStatusStrokeColor(riskStatus),
|
||||
}),
|
||||
padding: [5, 8, 5, 8],
|
||||
overflow: true,
|
||||
}),
|
||||
zIndex: 9996,
|
||||
});
|
||||
};
|
||||
|
||||
export const createWaterRiskPipeStyle = (feature: any) => {
|
||||
const capacityRatio = Number(feature.get("capacityRatio") ?? 0);
|
||||
const capacityColor = buildWaterRiskCapacityColor(capacityRatio);
|
||||
return [
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(255,255,255,0.98)",
|
||||
width: 9,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9997,
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: capacityColor,
|
||||
width: 4,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9998,
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
/** 맨홀 Flooding이 0이면 파랑, 양수이면 빨강으로 표시한다. */
|
||||
export const createWaterRiskManholeStyle = (feature: any) => {
|
||||
const floodingFlow = Number(feature.get("floodingFlow") ?? 0);
|
||||
const isFlooding = floodingFlow > 0;
|
||||
return new Style({
|
||||
image: new CircleStyle({
|
||||
radius: isFlooding ? 7 : 4,
|
||||
fill: new Fill({ color: isFlooding ? "#ff1c15" : "#1677ff" }),
|
||||
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
||||
}),
|
||||
zIndex: 9999,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
/** 지도 조립에서 사용하는 기존 레이어·메시지·표시 상수. 기존 값과 처리 순서를 유지한다. */
|
||||
|
||||
export const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
|
||||
export const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
|
||||
// 노후관로 지도 강조선과 시설물 트리의 선형 범례가 동일하게 사용하는 빨간색이다.
|
||||
export const AGED_PIPE_HIGHLIGHT_COLOR = "rgba(235, 40, 40, 0.82)";
|
||||
|
||||
export const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS";
|
||||
|
||||
// 심선은 동일 레이어 유형에 6개 깊이 등급 설정이 있어 prefix 뒤에 lyrSn을 붙여 각각 생성한다.
|
||||
export const FLDNG_DEPTH_WFS_LAYER_ID_PREFIX = "SN_FLDNG_DEPTH_WFS";
|
||||
|
||||
export const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
|
||||
export const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
|
||||
export const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
export const SINKHOLE_DEFAULT_POINT_LIMIT = 2000;
|
||||
|
||||
export const EXMN_PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||
|
||||
// 지반침하 지도 포인트와 같은 파란 계열의 기존 레이어 트리 심볼 자산을 재사용한다.
|
||||
export const SINKHOLE_TREE_ICON_NAME = "SN_SINKHOLE_SPOT_BLUE.png";
|
||||
|
||||
export const SOUTH_KOREA_VIEW_EXTENT: [number, number, number, number] = [
|
||||
600000, 1300000, 1500000, 2500000,
|
||||
];
|
||||
|
||||
export const EXMN_GRADE_COLORS: Record<number, string> = {
|
||||
1: "#22c55e",
|
||||
2: "#84cc16",
|
||||
3: "#facc15",
|
||||
4: "#f97316",
|
||||
5: "#dc2626",
|
||||
};
|
||||
|
||||
export const EXMN_RESULT_MANHOLE_ICON_SRC =
|
||||
"/images/map/layericon/SN_EXMN_MANHOLE_PURPLE.png?v=3";
|
||||
|
||||
export const FLOOD_PREDICTION_GRID_LAYER_ID = "SN_FLDNG_PREDICTION_GRID_LABEL";
|
||||
|
||||
/** 선택한 도시침수지도 중앙에 침수예측 분석값을 표시하는 오버레이 스타일이다. */
|
||||
// 유하방향은 GeoServer WMS/WFS 레이어가 아니라 BE 보정 GeoJSON API로 그리는 가상 레이어이다.
|
||||
export { FLOW_DIRECTION_PIPE_LAYER_TYPE, FLOW_DIRECTION_SRS } from "@/features/map-layers/model/flow-direction-runtime";
|
||||
|
||||
export const GIS_EDIT_PIPE_SNAP_BBOX_BUFFER_RATIO = 0.15;
|
||||
|
||||
export const GIS_EDIT_PREVIEW_LAYER_ID = "SN_GIS_EDIT_PREVIEW";
|
||||
|
||||
export const GIS_EDIT_VERTEX_LAYER_ID = "SN_GIS_EDIT_VERTEX_HANDLES";
|
||||
|
||||
export const WATER_RISK_AREA_LAYER_ID = "SN_WATER_RISK_AREA";
|
||||
|
||||
export const WATER_RISK_PIPE_LAYER_ID = "SN_WATER_RISK_PIPE";
|
||||
|
||||
export const WATER_RISK_MANHOLE_LAYER_ID = "SN_WATER_RISK_MANHOLE";
|
||||
|
||||
export const SHORT_TERM_FORECAST_GRID_LAYER_NAME = "초단기예보격자망";
|
||||
|
||||
export const FLDNG_HSTRY_SELECTION_PADDING: [number, number, number, number] = [
|
||||
100, 380, 120, 120,
|
||||
];
|
||||
|
||||
export const RIGHT_PANEL_TAB_LIMIT = 5;
|
||||
|
||||
// Only pipes opened from sinkhole details share one slot within the parent-return flow.
|
||||
export const RIGHT_PANEL_SINKHOLE_PIPE_TAB_ID =
|
||||
"feature:tb_ms_swage_ppl:sinkhole-singleton";
|
||||
|
||||
export const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [
|
||||
120, 120, 120, 120,
|
||||
];
|
||||
|
||||
export const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [
|
||||
50, 50, 50, 50,
|
||||
];
|
||||
|
||||
export const SINKHOLE_POINT_PALETTE = {
|
||||
orange: {
|
||||
fill: "rgba(214, 102, 31, 0.88)",
|
||||
fillSelected: "rgba(255, 138, 0, 0.92)",
|
||||
stroke: "rgba(255, 255, 255, 0.95)",
|
||||
strokeSelected: "rgba(120, 53, 15, 0.98)",
|
||||
radiusStroke: "rgba(214, 102, 31, 0.75)",
|
||||
radiusFill: "rgba(251, 146, 60, 0.12)",
|
||||
},
|
||||
blue: {
|
||||
fill: "rgba(37, 99, 235, 0.82)",
|
||||
fillSelected: "rgba(37, 99, 235, 0.9)",
|
||||
stroke: "rgba(255, 255, 255, 0.95)",
|
||||
strokeSelected: "rgba(30, 64, 175, 0.98)",
|
||||
radiusStroke: "rgba(37, 99, 235, 0.7)",
|
||||
radiusFill: "rgba(96, 165, 250, 0.12)",
|
||||
},
|
||||
} as const;
|
||||
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -11,15 +11,15 @@
|
||||
import React from "react";
|
||||
import { SelectBox } from "@common/business_components/ui/select";
|
||||
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 SwagePpl from "./swage-ppl";
|
||||
import SwagePrcsFclt from "./swage-prcs-fclt";
|
||||
import SwagePrcsZone from "./swage-prcs-zone";
|
||||
import FldngWtlvMap from "./fldng-wtlv-map";
|
||||
import SwageMhl from "./swage-mhl";
|
||||
import SwagePmstn from "./swage-pmstn";
|
||||
import Stofcbr from "./stofcbr";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
|
||||
// 기본정보 '구분' 선택 목록
|
||||
export const BASIC_TYPES = [
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
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";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
// 침수흔적도용 CQL 필터 생성
|
||||
export const fldngWtlvMapCqlFilters = () => {
|
||||
+6
-6
@@ -12,14 +12,14 @@
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from "react";
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { SelectBox } from "@common/business_components/ui/select";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import useStore from "@/store/useStore";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import useOperationTab from "./useOperationTab";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
import useOperationTab from "./use-operation-tab";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
|
||||
const OperationTab: React.FC = () => {
|
||||
const { BIZ_TY_CD } = useStore();
|
||||
+9
-9
@@ -15,16 +15,16 @@
|
||||
"use client";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Label } from "@common/components";
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { SelectBox } from "@common/business_components/ui/select";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import useReportTab from "./useReportTab";
|
||||
import useStore from "@/store/useStore";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import { useReportTabStore } from "@/store/useReportTabStore";
|
||||
import type { FlattenedFolder, ReportDataFile } from "./useReportTab";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import useReportTab from "./use-report-tab";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import { useReportTabStore } from "@/features/map-search/model/report-store";
|
||||
import type { FlattenedFolder, ReportDataFile } from "@/features/map-search/model/report-types";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
export const reportTableHeadlist = [
|
||||
+18
-21
@@ -12,33 +12,30 @@
|
||||
* - 로딩 오버레이 및 선택 초기화 지원
|
||||
*/
|
||||
"use client";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { UITab } from "@common/business_components/ui/tab";
|
||||
import { StandardPopupLayout } from "@common/business_components";
|
||||
import { Button } from "@common/components/ui";
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import {
|
||||
ExclnSewagePumpCol2,
|
||||
FldngWtlvMapCol2,
|
||||
FldngDpthMapCol2,
|
||||
MsSwageMhlCol2,
|
||||
MsSwagePplCol2,
|
||||
stofcbrCol2,
|
||||
SwtrmtFcltCol2,
|
||||
SwtrmtPrcsZoneCol2,
|
||||
} from "../components/common/table";
|
||||
import BasicTab, { BASIC_TYPES } from "./basicTab";
|
||||
import OperationTab from "./operationTab";
|
||||
import ReportTab from "./reportTab";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import useBasicTab from "./useBasicTab";
|
||||
import useOperationTab from "./useOperationTab";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import { ExclnSewagePumpCol2 } from "@/entities/facility/model/pump-columns";
|
||||
import { FldngWtlvMapCol2, FldngDpthMapCol2 } from "@/entities/facility/model/flood-columns";
|
||||
import { MsSwageMhlCol2 } from "@/entities/facility/model/manhole-columns";
|
||||
import { MsSwagePplCol2 } from "@/entities/facility/model/pipe-columns";
|
||||
import { stofcbrCol2 } from "@/entities/facility/model/overflow-columns";
|
||||
import { SwtrmtFcltCol2 } from "@/entities/facility/model/treatment-facility-columns";
|
||||
import { SwtrmtPrcsZoneCol2 } from "@/entities/facility/model/treatment-zone-columns";
|
||||
import BasicTab, { BASIC_TYPES } from "./basic-tab";
|
||||
import OperationTab from "./operation-tab";
|
||||
import ReportTab from "./report-tab";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
import useBasicTab from "./use-basic-tab";
|
||||
import useOperationTab from "./use-operation-tab";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
import type { UiHeadProps } from "@common/business_components/ui/table";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
|
||||
type SearchModalProps = {
|
||||
renderMode?: "launcher" | "popup";
|
||||
+4
-4
@@ -10,12 +10,12 @@
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { Button } from "@common/components";
|
||||
import React from "react";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
export const stofcbrCqlFilters = () => {
|
||||
const {
|
||||
+6
-5
@@ -12,13 +12,13 @@
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from "react";
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { MS_SWAGE_MHL_USE_CODES } from "../components/common/table";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import { SelectBox } from "@common/business_components/ui/select";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
export const swageMhlCqlFilters = () => {
|
||||
const {
|
||||
@@ -60,6 +60,7 @@ export const swageMhlCqlFilters = () => {
|
||||
};
|
||||
|
||||
const SwageMhl: React.FC = () => {
|
||||
const { MS_SWAGE_MHL_USE_CODES } = useFacilityCodeCatalog();
|
||||
const {
|
||||
var1,
|
||||
setVar1,
|
||||
+4
-4
@@ -11,12 +11,12 @@
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import React from "react";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
export const swagePmstnCqlFilters = () => {
|
||||
const {
|
||||
+6
-5
@@ -11,14 +11,14 @@
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { SelectBox } from "@common/business_components/ui/select";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import React from "react";
|
||||
import { MS_SWAGE_PPL_USE_CODES } from "../components/common/table";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
// 하수관로용 CQL 필터 생성
|
||||
export const swagePplCqlFilters = () => {
|
||||
@@ -67,6 +67,7 @@ export const swagePplCqlFilters = () => {
|
||||
};
|
||||
|
||||
const SwagePpl: React.FC = () => {
|
||||
const { MS_SWAGE_PPL_USE_CODES } = useFacilityCodeCatalog();
|
||||
const {
|
||||
var1,
|
||||
setVar1,
|
||||
+4
-4
@@ -12,11 +12,11 @@
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from "react";
|
||||
import { YearRangeSelectBox } from "@/components/ui/YearRangeSelectBox";
|
||||
import { YearRangeSelectBox } from "@/components/ui/year-range-select-box";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
// 하수처리시설용 CQL 필터 생성
|
||||
export const swagePrcsFcltCqlFilters = () => {
|
||||
+3
-3
@@ -11,10 +11,10 @@
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from "react";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import useSearchModalStore from "../../../store/useSearchModalStore";
|
||||
import useSearchModal from "./useSearchModal";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useSearchModal from "./use-search-modal";
|
||||
|
||||
// 하수처리구역용 CQL 필터 생성
|
||||
export const swagePrcsZoneCqlFilters = () => {
|
||||
+15
-20
@@ -13,30 +13,24 @@
|
||||
* - 관망도 시설물 목록 조회: GET /api/v1/fclts (sewerage-service)
|
||||
*/
|
||||
import { requestGridExcel } from "@/shared/api/grid-export";
|
||||
import {
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_SHAPE,
|
||||
MS_SWAGE_MHL_CODES,
|
||||
MS_SWAGE_MHL_USE_CODES,
|
||||
} from "../components/common/table";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { BASIC_TYPES } from "./basicTab";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import useStore from "@/store/useStore";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { BASIC_TYPES } from "./basic-tab";
|
||||
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
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 useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import { swagePplCqlFilters } from "./swage-ppl";
|
||||
import { swagePrcsFcltCqlFilters } from "./swage-prcs-fclt";
|
||||
import { swagePrcsZoneCqlFilters } from "./swage-prcs-zone";
|
||||
import { fldngWtlvMapCqlFilters } from "./fldng-wtlv-map";
|
||||
import { swageMhlCqlFilters } from "./swage-mhl";
|
||||
import { swagePmstnCqlFilters } from "./swage-pmstn";
|
||||
import { stofcbrCqlFilters } from "./stofcbr";
|
||||
import { postPopupMessageAndClose } from "@/features/windowPopup/lib/popupMessage";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import { postPopupMessageAndClose } from "@/features/window-popup/lib/popup-message";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
@@ -44,6 +38,7 @@ const isPopupWindow = () =>
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
export default function useBasicTab() {
|
||||
const { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES } = useFacilityCodeCatalog();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
const {
|
||||
+10
-12
@@ -16,19 +16,16 @@
|
||||
*/
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
} from "../components/common/table";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import { useFacilityCodeCatalog } from "@/entities/facility/lib/use-facility-code-catalog";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useStore from "@/store/useStore";
|
||||
import { postPopupMessageAndClose } from "@/features/windowPopup/lib/popupMessage";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import { postPopupMessageAndClose } from "@/features/window-popup/lib/popup-message";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import { useRef } from "react";
|
||||
|
||||
const isPopupWindow = () =>
|
||||
@@ -37,6 +34,8 @@ const isPopupWindow = () =>
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
|
||||
export default function useOperationTab() {
|
||||
const { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } =
|
||||
useFacilityCodeCatalog();
|
||||
const OPERATION_TABLE_SIZE = 5;
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { confirm } = useConfirm();
|
||||
@@ -173,8 +172,7 @@ export default function useOperationTab() {
|
||||
);
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber:
|
||||
(curPage - 1) * OPERATION_TABLE_SIZE + index + 1,
|
||||
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1,
|
||||
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
|
||||
};
|
||||
},
|
||||
+5
-24
@@ -13,33 +13,14 @@
|
||||
* - 보고서 파일 조회: GET /api/v1/file (dc-service)
|
||||
* - 파일 다운로드: GET /api/v1/fileDownload (dc-service)
|
||||
*/
|
||||
import { getDcData } from '@/components/lib/api';
|
||||
import { getDcData } from "@/shared/api/external-service-api";
|
||||
import { fileDownload } from '@/utils';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from "@/features/map-search/model/report-store";
|
||||
import useSearchModalStore from "@/features/map-search/model/search-store";
|
||||
import { useAlertStore } from '@/shared/lib';
|
||||
|
||||
export type Folder = {
|
||||
clsfSystmSn: string;
|
||||
upClsfSystmSn: string;
|
||||
clsfSystmNm: string;
|
||||
children: Folder[];
|
||||
};
|
||||
export type FlattenedFolder = {
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
export type SelectDataType = {
|
||||
text: string;
|
||||
val: string;
|
||||
sido?: string; // 시도 코드 (선택 사항)
|
||||
}
|
||||
export type ReportDataFile = {
|
||||
no: number;
|
||||
fileTitle: string;
|
||||
regDt: string;
|
||||
fileSn: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
import type { Folder, FlattenedFolder, SelectDataType, ReportDataFile } from "@/features/map-search/model/report-types";
|
||||
|
||||
const getStringValue = (value: string | number | undefined) => String(value ?? '');
|
||||
|
||||
+8
-11
@@ -12,18 +12,15 @@
|
||||
* 🔗 사용 API
|
||||
* - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service)
|
||||
*/
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import useStore from "../../../store/useStore";
|
||||
import { getData } from "@/shared/api/sewerage-api";
|
||||
import { formatDisplayData } from "@/components/lib/utils/format-display-value";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import useSearchModalStore, {
|
||||
SEARCH_MODAL_DEFAULT_BASIC_TYPE,
|
||||
SEARCH_MODAL_DEFAULT_WORK_TYPE,
|
||||
} from "../../../store/useSearchModalStore";
|
||||
import useBasicTab from "./useBasicTab";
|
||||
import useOperationTab from "./useOperationTab";
|
||||
import { postPopupMessageAndClose } from "@/features/windowPopup/lib/popupMessage";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import useSearchModalStore, { SEARCH_MODAL_DEFAULT_BASIC_TYPE, SEARCH_MODAL_DEFAULT_WORK_TYPE } from "@/features/map-search/model/search-store";
|
||||
import useBasicTab from "./use-basic-tab";
|
||||
import useOperationTab from "./use-operation-tab";
|
||||
import { postPopupMessageAndClose } from "@/features/window-popup/lib/popup-message";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
/** 지도 route에만 적용하는 기존 전역 컨트롤 CSS. 선택자와 스타일 값은 유지한다. */
|
||||
export default function MapControlStyles() {
|
||||
return (
|
||||
<style jsx global>{`
|
||||
.sn-map-control-layout .mapbtn {
|
||||
top: 16px;
|
||||
right: 120px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .map-type-tabs,
|
||||
.sn-map-control-layout .tool-bar ul,
|
||||
.sn-map-control-layout .zoom-level-panel,
|
||||
.sn-map-control-layout .zoom-level-summary {
|
||||
border: 0;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(60, 64, 67, 0.3),
|
||||
0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||
}
|
||||
|
||||
.sn-map-control-layout .custom-toolbar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar {
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar-vector-icons ul li div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: none !important;
|
||||
color: #666;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar-vector-icons ul li div:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-control {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-summary {
|
||||
right: 56px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .ol-scale-line {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ComponentProps } from "react";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
|
||||
/** 지도 검색바의 Radix Select는 브라우저 마운트 이후 생성한다.
|
||||
* 서버와 최초 클라이언트 렌더는 동일한 자리표시자를 사용하여 aria-controls ID의
|
||||
* hydration 불일치를 피한다. 공통 Select 및 다른 앱의 렌더링 방식은 변경하지 않는다.
|
||||
*/
|
||||
export default function MapSearchBar(props: ComponentProps<typeof ProcSearchBar>) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<div
|
||||
className="h-[44px] w-[656px] rounded-[8px] bg-form"
|
||||
role="status"
|
||||
aria-label="지도 검색조건 준비 중"
|
||||
aria-busy="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcSearchBar {...props} />;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isWindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
import { isWindowPopupKey } from "@/features/window-popup/model/popup-definitions";
|
||||
import {
|
||||
resolveWindowPopupTheme,
|
||||
WINDOW_POPUP_THEME_PARAM,
|
||||
} from "@/features/windowPopup/model/popupTheme";
|
||||
import PopupThemeSync from "@/features/windowPopup/ui/PopupThemeSync";
|
||||
import WindowPopupRenderer from "@/features/windowPopup/ui/WindowPopupRenderer";
|
||||
} from "@/features/window-popup/model/popup-theme";
|
||||
import PopupThemeSync from "@/features/window-popup/ui/popup-theme-sync";
|
||||
import WindowPopupRenderer from "@/features/window-popup/ui/window-popup-renderer";
|
||||
|
||||
type PopupPageProps = {
|
||||
params: Promise<{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
|
||||
type AnalysisResultPopupKey = "inftwtResultPopup" | "rdiiResultPopup";
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { DateMonthPicker, Form } from "@common/business_components/ui";
|
||||
import { Button, Label } from "@common/components/ui";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import type { ReactNode } from "react";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpv-sgg-prcs-fclt-select-box";
|
||||
|
||||
type TitleName = {
|
||||
mainTitle: string;
|
||||
@@ -16,8 +16,8 @@
|
||||
* - 로그인된 사용자의 사용자 정보 조회: GET /api/v1/users/info/me (user-service)
|
||||
*/
|
||||
"use client";
|
||||
import { postMutiPart } from "@/components/lib/api";
|
||||
import FileUploadForm from "@/components/ui/fileUploadForm";
|
||||
import { postMutiPart } from "@/shared/api/file-api";
|
||||
import FileUploadForm from "@/components/ui/file-upload-form";
|
||||
import {
|
||||
StandardFormField,
|
||||
StandardFormSection,
|
||||
@@ -34,15 +34,15 @@ import {
|
||||
excelToTimePatternData,
|
||||
excelToRcptrInfoData,
|
||||
} from "@/components/lib/utils";
|
||||
import BaseLayout from "../BaseLayout";
|
||||
import { useAnalysisCompletionWatcher } from "../useAnalysisCompletionWatcher";
|
||||
import { openAnalysisResultMapView } from "../analysisResultView";
|
||||
import BaseLayout from "../base-layout";
|
||||
import { useAnalysisCompletionWatcher } from "../use-analysis-completion-watcher";
|
||||
import { openAnalysisResultMapView } from "../analysis-result-view";
|
||||
import {
|
||||
validateMultipleFiles,
|
||||
FILE_EXTENSIONS,
|
||||
type FileValidationConfig,
|
||||
} from "../../fileValidation";
|
||||
import { validateBasicSelections } from "../../selectionValidation";
|
||||
} from "../../file-validation";
|
||||
import { validateBasicSelections } from "../../selection-validation";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { postMutiPart } from "@/components/lib/api";
|
||||
import FileUploadForm from "@/components/ui/fileUploadForm";
|
||||
import BaseLayout from "../BaseLayout";
|
||||
import { useAnalysisCompletionWatcher } from "../useAnalysisCompletionWatcher";
|
||||
import { openAnalysisResultMapView } from "../analysisResultView";
|
||||
import { postMutiPart } from "@/shared/api/file-api";
|
||||
import FileUploadForm from "@/components/ui/file-upload-form";
|
||||
import BaseLayout from "../base-layout";
|
||||
import { useAnalysisCompletionWatcher } from "../use-analysis-completion-watcher";
|
||||
import { openAnalysisResultMapView } from "../analysis-result-view";
|
||||
import { DateMonthPicker, filePicked } from "@common/business_components/ui";
|
||||
import {
|
||||
StandardFormField,
|
||||
@@ -40,13 +40,13 @@ import {
|
||||
import {
|
||||
EditableFormTable,
|
||||
type EditableTableHeadItem,
|
||||
} from "../../../components/ui/EditableFormTable";
|
||||
} from "../../../components/ui/editable-form-table";
|
||||
import {
|
||||
validateMultipleFiles,
|
||||
FILE_EXTENSIONS,
|
||||
type FileValidationConfig,
|
||||
} from "../../fileValidation";
|
||||
import { validateBasicSelections } from "../../selectionValidation";
|
||||
} from "../../file-validation";
|
||||
import { validateBasicSelections } from "../../selection-validation";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user