지도 대상 선택 이동 시 지역·시설 검색조건 초기화
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
@@ -91,6 +92,8 @@ export const useExmnResultMap = (options: Options) => {
|
||||
async (job: ExmnMapJobType, filters: ExmnMapFilters) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const navigation = entry.jobType === job && entry.exmnBizId && entry.trtdstId
|
||||
? beginMapNavigation(map) : null;
|
||||
removeLayer(job);
|
||||
const state = layers.current[job];
|
||||
const sequence = state.sequence;
|
||||
@@ -184,7 +187,7 @@ export const useExmnResultMap = (options: Options) => {
|
||||
const extent = selectedSource.getExtent();
|
||||
if (
|
||||
fittedContext.current !== contextKey &&
|
||||
extent.every(Number.isFinite)
|
||||
extent.every(Number.isFinite) && navigation?.commit(extent)
|
||||
) {
|
||||
map
|
||||
.getView()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 우측 정보 화면의 심선·위선 목록, 선택 상태와 화면 생명주기 캐시를 관리한다.
|
||||
*/
|
||||
"use client";
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
@@ -23,6 +24,7 @@ type FldngHstryGisLayer = {
|
||||
showSearchResultsFromGeoJSON: (
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
options?: { beforeFocus: (extent: unknown) => boolean },
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
@@ -127,11 +129,14 @@ const displayCachedFldngHstryFeatures = (
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
features: Feature[],
|
||||
beforeFocus: (extent: unknown) => boolean,
|
||||
) => {
|
||||
const matched = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === layerTypeName,
|
||||
) as any;
|
||||
if (!matched || features.length === 0) return;
|
||||
// 캐시를 재사용해도 최신 선택의 유효 도형일 때만 검색조건을 초기화한다.
|
||||
if (!beforeFocus(features[0]?.getGeometry()?.getExtent())) return;
|
||||
|
||||
gisLayer.clearSearchLayer(matched);
|
||||
const layer = gisLayer.ensureSearchLayer(matched);
|
||||
@@ -148,17 +153,20 @@ const showFldngHstryFeature = async (
|
||||
featureId: string,
|
||||
) => {
|
||||
const cacheKey = `${layerTypeName}:${featureId}`;
|
||||
/** 캐시 대기와 신규 WFS 조회가 동일한 이동 성공/취소 계약을 사용한다. */
|
||||
const navigationOptions = mapSearchNavigationOptions((GisApp.Module.core as any).map);
|
||||
const cachedRequest = cache.get(cacheKey);
|
||||
if (cachedRequest) {
|
||||
displayCachedFldngHstryFeatures(
|
||||
gisLayer,
|
||||
layerTypeName,
|
||||
await cachedRequest,
|
||||
navigationOptions.beforeFocus,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId);
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId, navigationOptions);
|
||||
const source = gisLayer.searchLayer?.getSource?.();
|
||||
if (!source) return;
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ const DEFAULT_FILTERS: InftwtResultFilters = {
|
||||
};
|
||||
|
||||
const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
// 같은 클릭의 message/storage 전달을 식별한다.
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
@@ -84,6 +86,7 @@ const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
JSON.stringify({
|
||||
row,
|
||||
timestamp: Date.now(),
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -96,6 +99,7 @@ const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
{
|
||||
type: INFTWT_RESULT_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
requestId,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { beginMapNavigation, fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
/** 침입수 분석결과 선택 시 부모 지도에 결과 레이어를 표시하는 훅이다. */
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
@@ -35,7 +36,6 @@ type UseInftwtResultMapParams = {
|
||||
const INFTWT_ZONE_LAYER_ID = "SN_INFTWT_RESULT_ZONE_LAYER";
|
||||
const INFTWT_PIPE_LAYER_ID = "SN_INFTWT_RESULT_PIPE_LAYER";
|
||||
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const INFTWT_PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||
|
||||
const zoneStyle = new Style({
|
||||
fill: new Fill({
|
||||
@@ -302,7 +302,7 @@ export const useInftwtResultMap = ({
|
||||
return;
|
||||
}
|
||||
|
||||
map.getView?.().fit?.(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 350,
|
||||
padding: DEFAULT_RESULT_FIT_PADDING,
|
||||
maxZoom: oilqPipeExtent ? 17 : shouldUseZoneExtent ? 15 : 16,
|
||||
@@ -319,7 +319,7 @@ export const useInftwtResultMap = ({
|
||||
if (normalizedSggCd && normalizedSggCd !== "0") {
|
||||
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
||||
if (sigunguCenter) {
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ export const useInftwtResultMap = ({
|
||||
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
||||
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
||||
if (sidoCenterCoordinate) {
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -395,8 +395,17 @@ export const useInftwtResultMap = ({
|
||||
[clearLayers, ensurePipeLayer, ensureZoneLayer, gisMaps, moveToStableFocus],
|
||||
);
|
||||
|
||||
/** message/storage가 전달하는 동일 선택 요청을 한 번만 처리한다. */
|
||||
const selectionRequestsRef = useRef(new Set<string>());
|
||||
const handleRowSelect = useCallback(
|
||||
async (row: InftwtResultRow) => {
|
||||
async (row: InftwtResultRow, requestId?: string) => {
|
||||
if (requestId) {
|
||||
const requests = selectionRequestsRef.current;
|
||||
if (requests.has(requestId)) return;
|
||||
requests.add(requestId);
|
||||
if (requests.size > 64) requests.delete(requests.values().next().value!);
|
||||
}
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
try {
|
||||
activeResultRowRef.current = row;
|
||||
setFeatureData(null);
|
||||
@@ -412,21 +421,9 @@ export const useInftwtResultMap = ({
|
||||
mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(INFTWT_PROC_SEARCH_EVENT, {
|
||||
detail: {
|
||||
ctpvCd: row.ctpvCd ?? "0",
|
||||
sggCd: row.sggCd ?? "0",
|
||||
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
||||
suppressMapMove: (mapView.oilqPipeCount ?? 0) > 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(mapView);
|
||||
void renderMapView(mapView);
|
||||
await renderMapView(mapView);
|
||||
} catch (error) {
|
||||
console.error("移⑥엯??遺꾩꽍寃곌낵 吏???곌퀎 以??ㅻ쪟:", error);
|
||||
alert("침입수 분석결과를 지도에 연계하지 못했습니다.");
|
||||
@@ -439,16 +436,16 @@ export const useInftwtResultMap = ({
|
||||
const handlePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== INFTWT_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
void handleRowSelect(event.data.row as InftwtResultRow);
|
||||
void handleRowSelect(event.data.row as InftwtResultRow, event.data.requestId);
|
||||
};
|
||||
|
||||
const handleStorageEvent = (event: StorageEvent) => {
|
||||
if (event.key !== INFTWT_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow };
|
||||
const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow; requestId?: string };
|
||||
if (!payload?.row) return;
|
||||
void handleRowSelect(payload.row);
|
||||
void handleRowSelect(payload.row, payload.requestId);
|
||||
} catch (error) {
|
||||
console.warn("移⑥엯??遺꾩꽍寃곌낵 storage ?숆린??媛믪쓣 ?댁꽍?섏? 紐삵뻽?듬땲??", error);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ const DEFAULT_FILTERS: RdiiResultFilters = {
|
||||
};
|
||||
|
||||
const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
// 같은 클릭의 message/storage 전달을 식별한다.
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
@@ -86,6 +88,7 @@ const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
JSON.stringify({
|
||||
row,
|
||||
timestamp: Date.now(),
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -98,6 +101,7 @@ const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
{
|
||||
type: RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
requestId,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** RDII 분석결과 선택 시 부모 지도에 결과 레이어를 표시하는 훅이다. */
|
||||
"use client";
|
||||
import { beginMapNavigation, fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
@@ -32,7 +33,6 @@ type UseRdiiResultMapParams = {
|
||||
|
||||
const RDII_PIPE_LAYER_ID = "SN_RDII_RESULT_PIPE_LAYER";
|
||||
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||
|
||||
const pipeStyle = [
|
||||
new Style({
|
||||
@@ -186,7 +186,7 @@ export const useRdiiResultMap = ({
|
||||
return;
|
||||
}
|
||||
|
||||
map.getView?.().fit?.(pipeExtent, {
|
||||
fitMapSelection(map, pipeExtent, {
|
||||
duration: 350,
|
||||
padding: DEFAULT_RESULT_FIT_PADDING,
|
||||
maxZoom: 16,
|
||||
@@ -201,7 +201,7 @@ export const useRdiiResultMap = ({
|
||||
if (normalizedSggCd && normalizedSggCd !== "0") {
|
||||
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
||||
if (sigunguCenter) {
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export const useRdiiResultMap = ({
|
||||
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
||||
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
||||
if (sidoCenterCoordinate) {
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -260,38 +260,37 @@ export const useRdiiResultMap = ({
|
||||
[clearLayers, ensurePipeLayer, gisMaps, moveToStableFocus],
|
||||
);
|
||||
|
||||
/** message/storage가 전달하는 동일 선택 요청을 한 번만 처리한다. */
|
||||
const selectionRequestsRef = useRef(new Set<string>());
|
||||
const handleRowSelect = useCallback(
|
||||
async (row: RdiiResultRow) => {
|
||||
async (row: RdiiResultRow, requestId?: string) => {
|
||||
if (requestId) {
|
||||
const requests = selectionRequestsRef.current;
|
||||
if (requests.has(requestId)) return;
|
||||
requests.add(requestId);
|
||||
if (requests.size > 64) requests.delete(requests.values().next().value!);
|
||||
}
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
try {
|
||||
activeResultRowRef.current = row;
|
||||
setFeatureData(null);
|
||||
setIsOpen(false);
|
||||
setIsSearchModalOpen(false);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PROC_SEARCH_EVENT, {
|
||||
detail: {
|
||||
ctpvCd: row.ctpvCd ?? "0",
|
||||
sggCd: row.sggCd ?? "0",
|
||||
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const cachedMapView = mapViewCacheRef.current.get(row.rainInfwtAnlsSn);
|
||||
if (cachedMapView) {
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(cachedMapView);
|
||||
void renderMapView(cachedMapView);
|
||||
await renderMapView(cachedMapView);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await getRawJson(`/sewerage-service/api/v1/rdiiAnlsRslt/mapView/${row.rainInfwtAnlsSn}`);
|
||||
const mapView = (payload?.data ?? payload) as RdiiResultMapViewResponse;
|
||||
mapViewCacheRef.current.set(row.rainInfwtAnlsSn, mapView);
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(mapView);
|
||||
void renderMapView(mapView);
|
||||
await renderMapView(mapView);
|
||||
} catch (error) {
|
||||
console.error("RDII 분석결과 지도 연계 중 오류:", error);
|
||||
alert("RDII 분석결과를 지도에 연계하지 못했습니다.");
|
||||
@@ -304,16 +303,16 @@ export const useRdiiResultMap = ({
|
||||
const handlePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== RDII_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
void handleRowSelect(event.data.row as RdiiResultRow);
|
||||
void handleRowSelect(event.data.row as RdiiResultRow, event.data.requestId);
|
||||
};
|
||||
|
||||
const handleStorageEvent = (event: StorageEvent) => {
|
||||
if (event.key !== RDII_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow };
|
||||
const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow; requestId?: string };
|
||||
if (!payload?.row) return;
|
||||
void handleRowSelect(payload.row);
|
||||
void handleRowSelect(payload.row, payload.requestId);
|
||||
} catch (error) {
|
||||
console.warn("RDII 분석결과 storage 동기화 값을 해석하지 못했습니다.", error);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import {
|
||||
@@ -462,26 +462,37 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
setSelectedResultKey("");
|
||||
};
|
||||
|
||||
/** 늦게 끝난 이전 행 요청이 최신 선택을 덮지 않도록 식별한다. */
|
||||
const mapRequestRef = useRef<string>("");
|
||||
|
||||
/** 선택 행의 실제 GeoJSON을 조회한 뒤 부모 지도 창에 전달한다. */
|
||||
const handleSelectResultRow = async (row: WaterRiskResultRow) => {
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
mapRequestRef.current = requestId;
|
||||
postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT, {
|
||||
phase: "loading", requestId,
|
||||
});
|
||||
setSelectedResultKey(buildResultRowKey(row));
|
||||
setIsMapLoading(true);
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const mapView = await fetchWaterRiskMapView(row);
|
||||
if (mapRequestRef.current !== requestId) return;
|
||||
postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT, {
|
||||
requestId,
|
||||
areaId: row.areaId,
|
||||
resultRow: row,
|
||||
mapView,
|
||||
});
|
||||
} catch (error) {
|
||||
if (mapRequestRef.current !== requestId) return;
|
||||
setErrorMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "통수위험 지도 결과를 조회하지 못했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setIsMapLoading(false);
|
||||
if (mapRequestRef.current === requestId) setIsMapLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { fitMapSelection } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
@@ -122,7 +124,7 @@ export function useFloodHistorySelection({
|
||||
if (!gisMaps.current) return;
|
||||
|
||||
const geometry = new Polygon([detail.areaRing]);
|
||||
gisMaps.current.getView?.().fit?.(geometry.getExtent(), {
|
||||
fitMapSelection(gisMaps.current, geometry.getExtent(), {
|
||||
duration: 350,
|
||||
padding: FLDNG_HSTRY_SELECTION_PADDING,
|
||||
maxZoom: 15,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import type { useFloodPredictionMap } from "@/features/fldng-predc-prst/lib/use-flood-prediction-map";
|
||||
import {
|
||||
useEffect,
|
||||
@@ -46,7 +48,7 @@ export function useMapRuntimeBridge({
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ export function useMapRuntimeBridge({
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions(gisMaps.current));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ export function useMapRuntimeBridge({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
map.getView().fit(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
@@ -35,6 +37,9 @@ export function useWaterRiskLayer({
|
||||
mapReady,
|
||||
onSelectResult,
|
||||
}: Options) {
|
||||
/** 팝업의 조회 시작부터 지도 이동까지 같은 요청 세대를 사용한다. */
|
||||
const selectionNavigationRef = useRef<ReturnType<typeof beginMapNavigation> | null>(null);
|
||||
const selectionRequestRef = useRef<string | undefined>(undefined);
|
||||
const waterRiskPopupWindowRef = useRef<Window | null>(null);
|
||||
|
||||
const waterRiskPopupCloseWatcherRef = useRef<number | null>(null);
|
||||
@@ -247,7 +252,7 @@ export function useWaterRiskLayer({
|
||||
const renderWaterRiskSelection = useCallback(
|
||||
(areaId: string | null, mapView: WaterRiskMapView | null) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
if (!map || !selectionNavigationRef.current?.isCurrent()) return;
|
||||
|
||||
const areaSource = ensureWaterRiskAreaLayer();
|
||||
if (!areaSource) return;
|
||||
@@ -318,6 +323,7 @@ export function useWaterRiskLayer({
|
||||
const extent = focusGeometry.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
setWaterRiskInfoAnchor([maxX, (minY + maxY) / 2]);
|
||||
if (!selectionNavigationRef.current?.commit(extent)) return;
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [120, 120, 120, 120],
|
||||
@@ -389,6 +395,17 @@ export function useWaterRiskLayer({
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT) {
|
||||
if (data.payload?.phase === "loading") {
|
||||
selectionRequestRef.current = data.payload.requestId;
|
||||
selectionNavigationRef.current = beginMapNavigation(gisMaps.current);
|
||||
return;
|
||||
}
|
||||
if (data.payload?.requestId) {
|
||||
if (data.payload.requestId !== selectionRequestRef.current ||
|
||||
!selectionNavigationRef.current?.isCurrent()) return;
|
||||
} else {
|
||||
selectionNavigationRef.current = beginMapNavigation(gisMaps.current);
|
||||
}
|
||||
const areaId = String(data.payload?.areaId ?? "").trim();
|
||||
if (!areaId) return;
|
||||
const resultRow = data.payload?.resultRow as
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useBasicTab Hook
|
||||
* 검색 모달 기본정보 탭 훅
|
||||
@@ -189,7 +190,7 @@ export default function useBasicTab() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useOperationTab Hook
|
||||
* 검색 모달 유지관리 정보 탭 훅
|
||||
@@ -284,7 +285,7 @@ export default function useOperationTab() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useSearchModal Hook
|
||||
* 검색 모달 훅
|
||||
@@ -198,7 +199,7 @@ export default function useSearchModal() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = tableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
@@ -222,7 +223,7 @@ export default function useSearchModal() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = detailTableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
"use client";
|
||||
import { fitMapSelection, mapSearchNavigationOptions, beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
import { useFloodPredictionMap } from "@/features/fldng-predc-prst/lib/use-flood-prediction-map";
|
||||
import {
|
||||
FLOOD_DNGR_MAP_LAYER_NAME,
|
||||
@@ -627,7 +628,7 @@ const MapPageClient = () => {
|
||||
const currentZoom = Number(view?.getZoom?.());
|
||||
const maxZoom =
|
||||
Number.isFinite(currentZoom) && currentZoom > 18 ? currentZoom : 18;
|
||||
view?.fit?.(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 350,
|
||||
padding: [80, 420, 80, 80],
|
||||
maxZoom,
|
||||
@@ -1292,6 +1293,7 @@ const MapPageClient = () => {
|
||||
void core?.showSearchResultsFromGeoJSON?.(
|
||||
layerTypeName,
|
||||
`${layerTypeName}.${featureIdentifier}`,
|
||||
mapSearchNavigationOptions(gisMaps.current),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1384,7 +1386,9 @@ const MapPageClient = () => {
|
||||
focusMap = true,
|
||||
fallbackPoint?: { x: number; y: number } | null,
|
||||
fallbackDetail?: Partial<SinkholeDetail> | null,
|
||||
resetSearch = false,
|
||||
) => {
|
||||
const navigation = resetSearch ? beginMapNavigation(gisMaps.current) : null;
|
||||
if (!acdntNo) return;
|
||||
|
||||
try {
|
||||
@@ -1540,6 +1544,7 @@ const MapPageClient = () => {
|
||||
}
|
||||
: detail;
|
||||
|
||||
if (navigation && !navigation.isCurrent()) return;
|
||||
setSelectedSinkholeDetail(normalizedDetail);
|
||||
setSelectedSinkholeNearbyPipes(nearbyPipes);
|
||||
renderSelectedSinkhole(normalizedDetail, sinkholeRadiusMeters);
|
||||
@@ -1555,6 +1560,9 @@ const MapPageClient = () => {
|
||||
normalizedDetail.x != null &&
|
||||
normalizedDetail.y != null
|
||||
) {
|
||||
const x = Number(normalizedDetail.x);
|
||||
const y = Number(normalizedDetail.y);
|
||||
if (navigation && !navigation.commit([x, y, x, y])) return;
|
||||
focusSinkholeOnMap(normalizedDetail, sinkholeRadiusMeters);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1615,6 +1623,7 @@ const MapPageClient = () => {
|
||||
|
||||
const focusSinkholeNearbyPipe = useCallback(
|
||||
async (pipe: SinkholeNearbyPipe) => {
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
const fid = String(pipe?.fid ?? "").trim();
|
||||
if (!fid) {
|
||||
alert("표시할 관로 식별값이 없습니다.");
|
||||
@@ -1654,7 +1663,7 @@ const MapPageClient = () => {
|
||||
});
|
||||
|
||||
const firstFeature = features[0];
|
||||
if (firstFeature) {
|
||||
if (firstFeature && navigation.commit(firstFeature.getGeometry()?.getExtent())) {
|
||||
syncRightPanelFeatureSelectionOnMap(firstFeature);
|
||||
setFeatureData(firstFeature);
|
||||
setIsOpen(true);
|
||||
@@ -2005,7 +2014,7 @@ const MapPageClient = () => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("노후관로 위치 이동 중 오류:", error);
|
||||
@@ -2025,7 +2034,7 @@ const MapPageClient = () => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("관로 위치 이동 중 오류:", error);
|
||||
@@ -2149,6 +2158,7 @@ const MapPageClient = () => {
|
||||
true,
|
||||
fallbackPoint,
|
||||
row,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ComponentProps } from "react";
|
||||
import { useCallback, useEffect, useState, useSyncExternalStore, type ComponentProps } from "react";
|
||||
import { getMapSearchRevision, invalidateMapNavigation, subscribeMapSearch } from "@/features/map-search/lib/map-navigation";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
|
||||
/** 지도 검색바의 Radix Select는 브라우저 마운트 이후 생성한다.
|
||||
@@ -8,6 +9,12 @@ import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
* hydration 불일치를 피한다. 공통 Select 및 다른 앱의 렌더링 방식은 변경하지 않는다.
|
||||
*/
|
||||
export default function MapSearchBar(props: ComponentProps<typeof ProcSearchBar>) {
|
||||
/** 부모의 지도 준비 렌더에 맞춰 해당 인스턴스만 구독한다. */
|
||||
const map = props.gisMaps.current;
|
||||
const subscribe = useCallback((listener: () => void) => subscribeMapSearch(map, listener), [map]);
|
||||
const getSnapshot = useCallback(() => getMapSearchRevision(map), [map]);
|
||||
const resetRevision = useSyncExternalStore(subscribe, getSnapshot, () => 0);
|
||||
useEffect(() => () => invalidateMapNavigation(map), [map]);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,5 +32,5 @@ export default function MapSearchBar(props: ComponentProps<typeof ProcSearchBar>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcSearchBar {...props} />;
|
||||
return <ProcSearchBar {...props} resetRevision={resetRevision} onSearchInteraction={() => invalidateMapNavigation(map)} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# SN GIS 지도 이동 검색조건 초기화 통합검증계획
|
||||
|
||||
## 2026-10-02 계획 / 실행 전
|
||||
|
||||
공통 설계의 15개 기능 묶음에 아래 항목을 적용한다. 원본 근거는 PPT 실제 슬라이드 19/SN_MN010201·21/SN_MN010202의 행 선택 지도 이동, 이번 사용자 확정은 상단 검색조건만 초기화이다.
|
||||
|
||||
| 확인 항목 | 기대 결과 |
|
||||
| --- | --- |
|
||||
| 서울특별시/강남구/시설 선택 후 통수위험 다른 지역 행 선택 | 기존 기준대로 이동, 상단 3개 선택은 기본값, 위험도·선택 강조·정보 카드 정상 유지 |
|
||||
| 필수 기능 각각의 행/셀 선택 | 동일 초기화. 새 결과의 범례/레이어/지도 확대 규칙 정상 |
|
||||
| 침입수 실측관로 있음/없음, RDII 지역 중심/extent fallback | 기존 최종 이동 유지, 지역/시설 자동 재채움 없음 |
|
||||
| 인라인 정보검색과 별도 창 정보검색 | 양쪽 모두 동일 동작. 팝업 origin 검증 유지 |
|
||||
| 조회 실패, geometry 없음, 빈 피처, NaN/무한/뒤집힌 extent | 초기화 및 잘못된 이동 없음. 기존 오류 안내 유지 |
|
||||
| 침수예측 레이어 준비 거부/취소 | 성공 이동이 없으면 상단 조건 유지 |
|
||||
| 이미 같은 위치인 행 재선택 | 이동할 대상이 유효하면 검색조건 초기화, moveend 발생 유무에 의존하지 않음 |
|
||||
| 상단 시도/시군구 조회, 상단 시설 선택 | 직접 선택한 검색값 유지. 공통 검색 함수 확장의 역효과 없음 |
|
||||
| 일반 드래그·줌·지도 피처 클릭 | 본 변경으로 검색조건 초기화하지 않음 |
|
||||
| 초기화 직전 시설 후보 요청을 느리게 응답 | 예전 시설 후보/선택/대기 시설명이 복원되지 않음 |
|
||||
| A→B 행을 빠르게 선택, A 응답이 나중 도착 | 마지막 선택만 유효. 이전 이동·검색바 변경 없음 |
|
||||
| message/storage로 같은 행 중복 도착 | 지도 이동/초기화 중복 처리 방지 |
|
||||
| 행 선택 후 응답 전에 사용자가 상단 조건 변경 | 오래된 작업이 최신 사용자 의도를 덮지 않음 |
|
||||
| 지도 미준비/검색바 마운트 전 메시지 | 초기화 의도 유실·다른 지도 영향 없음 |
|
||||
| 필터가 다른 하수처리시설 기본 라벨 | 현재 레이어 필터의 기본 라벨과 시설 유형 유지 |
|
||||
| GIS 편집 변경 목록 | 검색조건만 초기화, 미저장 변경/편집 세션 유지 |
|
||||
| 별도 준설 위치 지도·GIS 업로드 미리보기 | 메인 지도 검색조건에 영향 없음 |
|
||||
| 다른 ProcSearchBar/GIS 검색 함수 소비자 | optional 인자를 생략하면 이전과 같은 동작 |
|
||||
| 백엔드 요청 비교 | 선택 행 식별자·ctpvCd/sggCd·분석번호 보존. 검색바 초기값이 API 조건으로 섞이지 않음 |
|
||||
|
||||
## 검증 순서와 현재 결과
|
||||
|
||||
- 이번에는 정적 경로 추적과 원본 대조만 수행했다. 위 표의 동작 시험은 모두 미실행이다.
|
||||
- 구현 후 상태 초기화·응답 역전·유효 geometry 성공 callback 검증 → 대표 통수위험 브라우저 재현 → 기능별 회귀 순으로 진행한다.
|
||||
- 공통 패키지 변경은 SN 외 소비자의 기본값 보존을 함께 확인한다. 사용자 dev 서버와 build 동시 실행 금지.
|
||||
- 서버와 테스트를 실행한다면 적용 AGENTS.md의 소유 프로세스 종료·임시검증물 정리 규칙을 따른다.
|
||||
- 신규 버그/호출부 누락이 있으면 공통 설계 목록을 갱신한다. 코드 검색 결과를 실브라우저 통과로 기록하지 않는다.
|
||||
|
||||
## 2026-10-02 구현 후 진행 상태
|
||||
|
||||
- 자동/모의 실행 검증은 결과 문서 `002-qa-SN-GIS-지도이동검색조건초기화-통합검증결과.md`에 기록했다.
|
||||
- 실제 지도 로그인 후 기능별 회귀는 대기다. 위 표를 브라우저 통과로 일괄 표시하지 않는다.
|
||||
|
||||
## 2026-10-02 추가 회귀 대상
|
||||
|
||||
- 침수흔적 상세 심선/위선 연도 선택을 최초 조회와 캐시 재선택으로 나누어 검증한다. 캐시 대기 중 다른 대상을 선택하면 이전 요청이 검색바를 초기화하지 않아야 한다.
|
||||
- 상단 조건을 다시 선택한 뒤 침수흔적 상세 탭 복귀로 FID 재조회·지도 이동 시 초기화되는지 확인한다.
|
||||
- 모의 실행 결과는 결과 문서에 기록했고 실제 화면 확인은 미완료다.
|
||||
@@ -0,0 +1,51 @@
|
||||
# SN GIS 지도 이동 검색조건 초기화 통합검증결과
|
||||
|
||||
## 2026-10-02 자동검증 완료 / 실브라우저 대기
|
||||
|
||||
| 검증 | 결과 |
|
||||
| --- | --- |
|
||||
| SN 전체 TypeScript (`node node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false`) | 최종 exit 0. 초기 sandbox EPERM 후 승인된 외부 실행, 중간 타입 차이 수정 후 최종 통과 |
|
||||
| 지도 공통 모듈 실제 함수 실행 | 초기화 1회/지도별 격리/빈·NaN·무한·역전 범위 거부/이전 요청 취소/검색바 조작 우선/동일 위치 재선택/구독 해제 통과 |
|
||||
| 실제 gis.layer 메서드와 모의 source 이벤트 실행 | 로드 전 초기화 없음/도형 로드 후 callback/취소/옵션 없는 기존 호출/이전 source 무시/다중 선택 성공·취소 통과 |
|
||||
| 침입수/RDII 실제 송신 함수 및 중심 이동 함수 실행 | message/storage 동일 ID, 같은 행 재클릭 새 ID, crypto 의존 없음, 중심 이동 승인·취소·기존 호출·비정상 좌표 거부 통과 |
|
||||
| ProcSearchBar 로직 hook 모의 실행 | 상단 직접 조회값 유지, 3개 선택값·후보 초기화, 늦은 시설 응답 무시, 초기화 자체의 지도 이동 없음, 시설 기본 라벨 유지 통과 |
|
||||
| UTF-8/클라이언트 지시문 순서/diff 공백 | 확인 통과 |
|
||||
| 실 API/DB | 미수행. BE 코드·계약·DB 변경 없음 |
|
||||
| 브라우저 | localhost:3002/map 진입 후 /auth/login으로 이동. 로그인 대기. 아직 실제 지도 동작 통과 아님 |
|
||||
| build | 사용자 dev 서버 실행 중이므로 미수행 |
|
||||
|
||||
- Node 검증은 실제 소스에서 함수/로직을 읽어 모의 지도·네트워크·hook 상태로 실행했다. 실제 React DOM/OpenLayers/GeoServer를 연결한 브라우저 통합시험을 대체하지 않는다.
|
||||
- 검증용 파일·서버를 만들지 않았다. 사용자 서버 3002/PID 40220 및 8883/PID 2676 보존.
|
||||
- 원본 근거: 요구사항 기능 시트 93/SFR-011-007, 96/SFR-012-001, PPT 실제 슬라이드 19/SN_MN010201·21/SN_MN010202.
|
||||
- 남은 검증: 통합검증계획의 기능별 실데이터 회귀와 다른 공통 컴포넌트 소비자의 화면 동작.
|
||||
|
||||
## 2026-10-02 사용자 요청에 따른 적용 경로 재점검
|
||||
|
||||
- 이전 `15개 기능 적용` 보고는 대표 목록 경로 기준이었다. 재점검에서 침수흔적 상세 내부의 추가 선택 경로가 누락된 것을 발견하여 보완했다. 이전 보고를 모든 하위 동작의 실검증 완료로 해석하지 않는다.
|
||||
- 보완 1: `components/fldng-hstry/fldng-hstry-feature-click.tsx`의 심선·위선 연도 선택. 신규 WFS 조회는 beforeFocus를 전달하고 캐시 재사용은 유효 도형/최신 요청 확인 후 초기화한다.
|
||||
- 보완 2: `ui/map-page-client.tsx`의 침수흔적 상세 탭 복귀 시 FID 재조회. 같은 성공 callback을 연결했다.
|
||||
- 본 라운드 수정 소스 2개. 누적 소스는 기존21+신규1=22개다. API·DB 변경 없음.
|
||||
|
||||
| 대조한 기능 | 초기화 연결 근거 |
|
||||
| --- | --- |
|
||||
| 통수위험 현재/과거 | use-water-risk-layer의 selectionNavigationRef.commit → fit |
|
||||
| 침입수·RDII | 각 결과 hook의 fitMapSelection / 지역중심 beforeMove |
|
||||
| 노후관로·관로분석 상세 | map-page-client의 두 행 선택 handler → mapSearchNavigationOptions |
|
||||
| 정보검색 기본/유지관리 결과 | use-basic-tab / use-operation-tab의 직접 검색 및 runtime bridge의 popup 수신 |
|
||||
| 정보검색 선택/전체 지도보기 | use-search-modal / runtime bridge의 다중 검색 callback |
|
||||
| 지반침하 목록·주변 관로 | loadSinkholeDetail의 resetSearch=true / focusSinkholeNearbyPipe의 commit |
|
||||
| 침수발생이력 목록 | runtime bridge의 일반 FID 검색 / use-flood-history-selection의 fitMapSelection |
|
||||
| 침수위험지도 중점관리지역 | MAP_SEARCH_SHOW_MULTIPLE → runtime bridge |
|
||||
| 도시침수지도·침수예측현황 | MAP_SEARCH_SHOW_FLOOD_GROUP → use-flood-prediction-map의 commit |
|
||||
| 처리구역 보수이력 | swtrmt-prcs-zone-analysis의 fitMapSelection |
|
||||
| GIS 편집 변경 목록 | focusGisEditChangeOnMap의 fitMapSelection |
|
||||
| 조사결과 지도보기 | use-exmn-result-map의 navigation.commit → fit |
|
||||
| 맨홀 상류관로 | smh의 fitMapSelection |
|
||||
| 침수흔적 상세 심선·위선 | 이번 보완: 신규 WFS callback + 캐시 beforeFocus |
|
||||
| 침수흔적 상세 탭 복귀 재조회 | 이번 보완: FID 검색 callback |
|
||||
|
||||
- `fit`/optional fit/animate/setCenter/지역중심 함수/단일·다중 GIS 검색/피처 선택 및 메시지 경로를 SN TS·TSX·JS·JSX 범위에서 다시 검색했다. 직접 fit 잔여는 상위 commit으로 보호되거나 아래 제외 경로임을 확인했다.
|
||||
- 의도적 제외: 상단 직접 검색/시설 선택, 일반 지도 클릭·드래그·줌, GIS 편집 저장 후 자동 재초점, 별도 GIS 업로드 미리보기·준설 위치 지도. 원래 위치를 이동하지 않는 상세 탭 선택/업로드 이력 선택도 강제 초기화하지 않는다.
|
||||
- 검증: SN 전체 TypeScript 최종 exit 0, diff 공백 검사 통과. 실제 침수흔적 helper를 읽어 모의 실행한 캐시 성공/늦은 캐시 응답 거부/신규 조회 callback 전달 시험 통과. 테스트 파일/프로세스 생성 없음.
|
||||
- 현재 결론은 정적 적용 경로 및 자동검증 기준이다. 로그인 이후 전 기능 실브라우저 검증은 여전히 별도 미완료다.
|
||||
- 이번 보완 롤백: 위 2개 소스의 beforeFocus 추가만 복원. 전체 기능 롤백은 누적22개 변경을 함께 복원하며 DB 작업은 없다.
|
||||
@@ -0,0 +1,62 @@
|
||||
# SN GIS 지도 이동 검색조건 초기화 통합구현기록
|
||||
|
||||
## 2026-10-02 유지보수 사전 분석 — 구현 미착수
|
||||
|
||||
- 사용자 범위: 프론트+백엔드 영향 분석. 실제 구현은 다음 단계.
|
||||
- 확정 대상: 상단 지역·시설 검색조건. 목록에서 선택한 분석 결과/레이어 상태 유지.
|
||||
- 원본: 요구사항정의서 기능 시트 93행 SFR-011-007, 96행 SFR-012-001; 화면설계서 실제 슬라이드 19/SN_MN010201, 21/SN_MN010202.
|
||||
- `ProcSearchBar` 내부 state와 공통 GeoJSON 검색/직접 fit의 분산 구조를 확인했다. 통수위험은 검색바 갱신이 없고 침입수/RDII는 반대로 지역·시설 동기화 이벤트가 있다.
|
||||
- 필수 대상 15개 기능 묶음과 별도 버튼/지도 진입 경로는 공통 설계의 목록을 기준으로 한다. 기능 목록은 정적 코드 기준이며 브라우저에서 전수 검증한 목록이 아니다.
|
||||
- 제안: SN 지도별 공통 이동 처리, 검색바 optional reset revision, 공통 GeoJSON 검색의 optional 성공 callback. `moveend` 일괄 초기화 금지.
|
||||
- 구현 시 핵심 파일: `map/ui/map-search-bar.tsx`, `map/ui/map-page-client.tsx`, `map/lib/use-map-runtime-bridge.ts`, 각 결과 지도 hook, `src/features/facility-detail`의 직접 이동부. 허용 공통 경로는 `packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx` 및 필요한 `lib/gis.layer.js`로 한정한다.
|
||||
- 비동기 시설 후보 조회의 늦은 응답, message/storage 중복, 실패 시 유지, 검색바 직접 조회의 값 보존을 검증한다.
|
||||
- 이번 실제 수정은 문서뿐이다. 타입검사·빌드·브라우저·API 시험은 미수행이며 새 테스트 파일/서버를 생성하지 않았다.
|
||||
- 기존 문서 경로와 파일명은 유지했다. 기존 한글 주석의 일부 손상은 본 작업에서 수정하지 않았다.
|
||||
- 롤백: 이 기록 및 공통 설계/검증계획의 이번 추가분만 복원. 코드·API·DB 영향 없음.
|
||||
- 상세 설계: `IIPBS/docs/작업계획/SN-GIS-지도이동검색조건초기화/001-de-SN-GIS-지도이동검색조건초기화-설계.md`.
|
||||
|
||||
## 2026-10-02 구현 및 자동검증
|
||||
|
||||
- 사용자 후속 구현 지시에 따라 공통 지도 이동/검색조건 초기화를 반영했다.
|
||||
- 지도별 요청 세대와 reset revision, 검색바 optional props, GIS 비동기 성공 callback, 직접 fit/지역 이동 경로를 연결했다.
|
||||
- 기존 분석 결과·선택 강조·레이어·시설유형/필터·상단 직접 검색 동작은 유지한다. 실행 경로별 보존은 브라우저 회귀에서 추가 확인한다.
|
||||
- 침입수/RDII 기존 검색바 채우기 제거, 선택 요청 ID 중복 제거, 통수위험의 조회 시작/결과 식별과 오래된 응답 방어를 구현했다.
|
||||
- 타입검사 최종 exit 0, 파일 없는 Node 실행 검증 4종 및 diff 공백 검사 통과. 중간의 FitOptions/부분 지도 인터페이스 타입 오류는 수정 후 재검사했다.
|
||||
- 사용자 개발 서버 가동을 확인하여 build/재시작하지 않았다. 새 프로세스/검증파일 생성 없음. 브라우저 로그인 대기.
|
||||
|
||||
### 실제 소스 변경 파일 (frontend 기준)
|
||||
|
||||
- `apps/sn/app/(map-layout)/map/components/exmn/use-exmn-result-map.ts`
|
||||
- `apps/sn/app/(map-layout)/map/components/inftwt-result/inftwt-result-popup-page.tsx`
|
||||
- `apps/sn/app/(map-layout)/map/components/inftwt-result/use-inftwt-result-map.ts`
|
||||
- `apps/sn/app/(map-layout)/map/components/rdii-result/rdii-result-popup-page.tsx`
|
||||
- `apps/sn/app/(map-layout)/map/components/rdii-result/use-rdii-result-map.ts`
|
||||
- `apps/sn/app/(map-layout)/map/components/water-risk/water-risk-popup-page.tsx`
|
||||
- `apps/sn/app/(map-layout)/map/lib/use-flood-history-selection.ts`
|
||||
- `apps/sn/app/(map-layout)/map/lib/use-map-runtime-bridge.ts`
|
||||
- `apps/sn/app/(map-layout)/map/lib/use-water-risk-layer.ts`
|
||||
- `apps/sn/app/(map-layout)/map/search-modal/use-basic-tab.ts`
|
||||
- `apps/sn/app/(map-layout)/map/search-modal/use-operation-tab.ts`
|
||||
- `apps/sn/app/(map-layout)/map/search-modal/use-search-modal.ts`
|
||||
- `apps/sn/app/(map-layout)/map/ui/map-page-client.tsx`
|
||||
- `apps/sn/app/(map-layout)/map/ui/map-search-bar.tsx`
|
||||
- `apps/sn/src/features/facility-detail/ui/smh.tsx`
|
||||
- `apps/sn/src/features/facility-detail/ui/swtrmt-prcs-zone-analysis.tsx`
|
||||
- `apps/sn/src/features/fldng-predc-prst/lib/use-flood-prediction-map.ts`
|
||||
- `packages/common/business_components/src/ui/map/lib/gis.layer.js`
|
||||
- `packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx`
|
||||
- `packages/common/business_components/src/ui/map/searchBar/utils.ts`
|
||||
- `apps/sn/src/features/map-search/lib/map-navigation.ts`
|
||||
|
||||
### 남은 검증과 롤백
|
||||
|
||||
- 실제 데이터로 15개 기능의 지도 이동/상단 초기화 및 분석 표시 보존 확인은 로그인 후 수행한다.
|
||||
- 롤백 시 위 소스 변경을 함께 복원한다. BE/DB 롤백 없음.
|
||||
|
||||
## 2026-10-02 적용 누락 재점검 및 보완
|
||||
|
||||
- 사용자 재확인 요청으로 전체 지도 이동 호출·메시지 경로를 대조했다. 침수흔적 상세 심선/위선 연도 선택(신규 조회·캐시)과 상세 탭 복귀 FID 재조회 경로의 초기화 누락을 발견해 보완했다.
|
||||
- 수정: `frontend/apps/sn/app/(map-layout)/map/components/fldng-hstry/fldng-hstry-feature-click.tsx`, `frontend/apps/sn/app/(map-layout)/map/ui/map-page-client.tsx`. 누적 소스 변경은22개다.
|
||||
- 기존15개 기능 묶음 및 추가 경로의 적용표를 공통 설계/통합검증결과에 기록했다. 초기 대표 목록 적용 보고와 하위 상세 경로 누락을 구분했다.
|
||||
- SN 전체 타입검사 exit0, 캐시 성공·지연 응답 거부·신규 callback 전달 실행검증 및 diff 공백 검사 통과. 임시파일·서버 생성 없음, BE/API/DB 변경 없음.
|
||||
- 실브라우저 전수 검증은 로그인 대기 상태로 유지한다. 롤백은 위2개 파일의 이번 callback 추가를 복원한다.
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fitMapSelection } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 화면명: 하수맨홀 정보
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수맨홀)
|
||||
@@ -69,7 +70,7 @@ export async function searchUpperPpls(
|
||||
const geometry = foundFeature?.getGeometry();
|
||||
if (geometry) {
|
||||
const extent = geometry.getExtent();
|
||||
gisMap.getView().fit(extent, {
|
||||
fitMapSelection(gisMap, extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 14,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 선택한 하수처리구역의 관로 분석 현황과 보수이력 연계 기능을 제공한다.
|
||||
*/
|
||||
"use client";
|
||||
import { fitMapSelection } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -228,7 +229,7 @@ const SwtrmtPrcsZoneAnalysis = ({
|
||||
repairLayerRef.current?.changed();
|
||||
const extent = selectedFeature.getGeometry()?.getExtent();
|
||||
if (extent) {
|
||||
gisMap.getView().fit(extent, {
|
||||
fitMapSelection(gisMap, extent, {
|
||||
padding: [100, 100, 100, 460],
|
||||
duration: 350,
|
||||
maxZoom: 18,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -217,6 +218,8 @@ export const useFloodPredictionMap = ({
|
||||
floodLayerType,
|
||||
onRequestFloodLayer,
|
||||
}: UseFloodPredictionMapOptions) => {
|
||||
/** 레이어 준비 대기와 늦은 오버레이 응답도 같은 선택 세대로 제한한다. */
|
||||
const navigationRef = useRef<ReturnType<typeof beginMapNavigation> | null>(null);
|
||||
// 자동 레이어 선택이 완료된 뒤 다시 적용할 침수예측 행을 보관한다.
|
||||
const [pendingSelection, setPendingSelection] =
|
||||
useState<FloodPredictionSelection | null>(null);
|
||||
@@ -241,6 +244,7 @@ export const useFloodPredictionMap = ({
|
||||
/** 선택행의 시군구·분석시각으로 도시침수지도 중앙 오버레이를 표시한다. */
|
||||
const loadFloodPredictionOverlay = useCallback(
|
||||
async (selection: FloodPredictionSelection) => {
|
||||
const navigation = navigationRef.current;
|
||||
const { sggCd, analysisDateTime, extent } = selection;
|
||||
const map = mapRef.current;
|
||||
if (!map || !/^\d{12}$/.test(analysisDateTime)) return;
|
||||
@@ -250,6 +254,7 @@ export const useFloodPredictionMap = ({
|
||||
const response = await getRawJson(
|
||||
`/sewerage-service/api/v1/fldng/predc/prst/gridMap?sggCd=${encodeURIComponent(sggCd)}&analysisDateTime=${encodeURIComponent(analysisDateTime)}`,
|
||||
);
|
||||
if (!navigation?.isCurrent()) return;
|
||||
const payload = response?.data ?? response;
|
||||
const row =
|
||||
payload && typeof payload === "object" && !Array.isArray(payload)
|
||||
@@ -284,6 +289,8 @@ export const useFloodPredictionMap = ({
|
||||
/** 활성화된 도시침수지도에서 선택 행의 시군구·빈도 피처를 선택하고 지도와 오버레이를 이동한다. */
|
||||
const applyFloodPredictionSelection = useCallback(
|
||||
async (selection: FloodPredictionSelection) => {
|
||||
const navigation = navigationRef.current;
|
||||
if (!navigation?.isCurrent()) return true;
|
||||
const map = mapRef.current;
|
||||
const select = GisApp.Module.select as unknown as
|
||||
| FloodSelectionModule
|
||||
@@ -302,10 +309,12 @@ export const useFloodPredictionMap = ({
|
||||
);
|
||||
if (floodLayers.length === 0) return false;
|
||||
|
||||
await select.resetSelectedFeature?.();
|
||||
if (!navigation.commit(selection.extent)) return true;
|
||||
select.resetSelectedFeature?.();
|
||||
const selectedFeatureIds = new Set<string>();
|
||||
/** 현재 로드된 동일 시군구·빈도 피처를 중복 없이 선택한다. */
|
||||
const selectLoadedGroupFeatures = (source: FloodPredictionSource) => {
|
||||
if (!navigation.isCurrent()) return;
|
||||
const features = source?.getFeatures?.() ?? [];
|
||||
features.forEach((feature) => {
|
||||
const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim();
|
||||
@@ -352,6 +361,8 @@ export const useFloodPredictionMap = ({
|
||||
async (payload: unknown, popupContext: FloodPredictionPopupContext) => {
|
||||
const selection = parseFloodPredictionSelection(payload);
|
||||
if (!selection) return;
|
||||
navigationRef.current = beginMapNavigation(mapRef.current);
|
||||
setPendingSelection(null);
|
||||
if (await applyFloodPredictionSelection(selection)) return;
|
||||
|
||||
pendingPopupRef.current = popupContext;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type Map from "ol/Map";
|
||||
import type { FitOptions } from "ol/View";
|
||||
|
||||
/** 지도 객체별 이동 요청과 검색바 초기화 revision을 보관한다. 다른 지도와 상태를 공유하지 않는다. */
|
||||
type NavigationState = {
|
||||
generation: number;
|
||||
revision: number;
|
||||
listeners: Set<() => void>;
|
||||
};
|
||||
const states = new WeakMap<object, NavigationState>();
|
||||
/** 부분 지도 인터페이스를 사용하는 기능도 동일한 인스턴스로 요청을 관리한다. */
|
||||
type NavigationTarget = { getView?: () => unknown };
|
||||
|
||||
/** 지도 생명주기와 함께 회수되는 상태를 반환한다. */
|
||||
function stateFor(map: object): NavigationState {
|
||||
let state = states.get(map);
|
||||
if (!state) {
|
||||
state = { generation: 0, revision: 0, listeners: new Set() };
|
||||
states.set(map, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 빈/역전/비수치 범위를 이동 성공으로 취급하지 않는다. */
|
||||
export function isNavigationExtent(extent: unknown): extent is number[] {
|
||||
return Array.isArray(extent) && extent.length === 4 &&
|
||||
extent.every(Number.isFinite) && extent[0] <= extent[2] && extent[1] <= extent[3];
|
||||
}
|
||||
|
||||
/** 검색바 조작은 아직 완료되지 않은 목록 이동보다 우선한다. */
|
||||
export function invalidateMapNavigation(map: object | null | undefined) {
|
||||
if (map) stateFor(map).generation += 1;
|
||||
}
|
||||
|
||||
/** 검색바는 이미 발생한 초기화도 revision으로 읽어 늦은 마운트에서 유실하지 않는다. */
|
||||
export function getMapSearchRevision(map: object | null | undefined) {
|
||||
return map ? stateFor(map).revision : 0;
|
||||
}
|
||||
|
||||
/** 한 지도 검색바의 초기화만 구독한다. */
|
||||
export function subscribeMapSearch(map: object | null | undefined, listener: () => void) {
|
||||
if (!map) return () => {};
|
||||
const state = stateFor(map);
|
||||
state.listeners.add(listener);
|
||||
return () => { state.listeners.delete(listener); };
|
||||
}
|
||||
|
||||
/** 조회 시작에 발급하고 도형 확보 뒤 commit한다. 최신 요청만 한 번 검색바를 초기화한다. */
|
||||
export function beginMapNavigation(map: NavigationTarget | null | undefined) {
|
||||
const state = map ? stateFor(map) : null;
|
||||
const generation = state ? ++state.generation : -1;
|
||||
let committed = false;
|
||||
const isCurrent = () => Boolean(state && state.generation === generation);
|
||||
return {
|
||||
isCurrent,
|
||||
commit(extent: unknown) {
|
||||
if (!isCurrent() || !map?.getView?.() || !isNavigationExtent(extent)) return false;
|
||||
if (!committed && state) {
|
||||
committed = true;
|
||||
state.revision += 1;
|
||||
state.listeners.forEach((listener) => listener());
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 도형이 이미 준비된 목록 선택은 기존 fit 옵션을 그대로 사용한다. */
|
||||
export function fitMapSelection(map: Pick<Map, "getView"> | null | undefined, extent: unknown, options: FitOptions & { constrainResolution?: boolean }) {
|
||||
if (!map?.getView?.()?.fit || !isNavigationExtent(extent)) return false;
|
||||
const navigation = beginMapNavigation(map);
|
||||
if (!navigation.commit(extent)) return false;
|
||||
map.getView().fit(extent, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 공통 GeoJSON 검색의 비동기 로드 완료 시 호출할 선택적 계약이다. */
|
||||
export function mapSearchNavigationOptions(map: NavigationTarget | null | undefined) {
|
||||
const navigation = beginMapNavigation(map);
|
||||
return { beforeFocus: (extent: unknown) => navigation.commit(extent) };
|
||||
}
|
||||
@@ -659,7 +659,7 @@ GisOlLayer.prototype = {
|
||||
this.searchLayer = null;
|
||||
}
|
||||
},
|
||||
showSearchResultsFromGeoJSON: function (lyrTyNm, featureId) {
|
||||
showSearchResultsFromGeoJSON: function (lyrTyNm, featureId, options) {
|
||||
//레이어 타입 설정(point, line 등. 이걸 style에서 사용)
|
||||
const layerCode = GisApp.layerCode;
|
||||
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
|
||||
@@ -674,10 +674,15 @@ GisOlLayer.prototype = {
|
||||
|
||||
// 1) 로드 완료시 extent 맞추기
|
||||
const onLoadEnd = () => {
|
||||
// 이전 검색 source의 지연 응답은 현재 선택을 변경하지 않는다.
|
||||
if (layer.getSource() !== source || this.searchLayer !== layer) return;
|
||||
if (Object.keys(source.nullGeometryFeatures_).length > 0) showBusinessUserMessage('해당 시설의 위치 정보가 없습니다.');
|
||||
|
||||
const feats = source.getFeatures();
|
||||
if (feats && feats.length > 0) {
|
||||
const extent = feats[0].getGeometry()?.getExtent();
|
||||
// 선택적 소비자만 성공 경계에서 초기화/취소를 결정한다.
|
||||
if (options?.beforeFocus && options.beforeFocus(extent) === false) return;
|
||||
const select = GisApp.Module.select;
|
||||
select.selectFeature(feats[0], false);
|
||||
} else {
|
||||
@@ -740,7 +745,7 @@ GisOlLayer.prototype = {
|
||||
* @param {string} lyrTyNm 레이어 타입명
|
||||
* @param {Array|string} featureIds 피처 ID 배열 또는 단일 ID
|
||||
*/
|
||||
showMultipleSearchResultsFromGeoJSON: function (lyrTyNm, featureIds) {
|
||||
showMultipleSearchResultsFromGeoJSON: function (lyrTyNm, featureIds, options) {
|
||||
|
||||
const layerCode = GisApp.layerCode;
|
||||
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
|
||||
@@ -754,10 +759,13 @@ GisOlLayer.prototype = {
|
||||
const source = this.getMultiFeatureSourceFromGeoserver(lyrTyNm, featureIds);
|
||||
|
||||
const onLoadEnd = async () => {
|
||||
if (layer.getSource() !== source || this.searchLayer !== layer) return;
|
||||
const feats = source.getFeatures();
|
||||
if (feats && feats.length > 0) {
|
||||
const focusExtent = this.combineExtents(feats.map(f => f.getGeometry()?.getExtent()));
|
||||
if (options?.beforeFocus && options.beforeFocus(focusExtent) === false) return;
|
||||
const select = GisApp.Module.select;
|
||||
await select.resetSelectedFeature();
|
||||
select.resetSelectedFeature();
|
||||
for (const feature of feats) select.selectMultiFeature(feature, true);
|
||||
const geometries = feats.map(f => f.getGeometry());
|
||||
if (geometries.length > 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { SelectBox, type SelectDataType } from "../../../ui";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { showBusinessUserMessage } from "../../../lib/user-message";
|
||||
import GisApp from "../lib/gis.core";
|
||||
import {
|
||||
@@ -34,6 +34,10 @@ interface SearchBarProps {
|
||||
facilityLayerFilter?: FacilityLayerFilter;
|
||||
/** 관망도 전용 3개 셀렉트와 조회 버튼을 하나로 연결한 검색바 레이아웃이다. */
|
||||
integrated?: boolean;
|
||||
/** 지도 목록 선택이 성공했을 때만 증가하는 선택적 초기화 revision이다. */
|
||||
resetRevision?: number;
|
||||
/** 직접 검색조건 조작이 이전 비동기 지도 선택을 취소할 수 있게 알린다. */
|
||||
onSearchInteraction?: () => void;
|
||||
}
|
||||
|
||||
interface CenterSelectDataType extends SelectDataType {
|
||||
@@ -46,7 +50,13 @@ export const ProcSearchBar = ({
|
||||
sidoSearchZoom = 11,
|
||||
facilityLayerFilter,
|
||||
integrated = false,
|
||||
resetRevision,
|
||||
onSearchInteraction,
|
||||
}: SearchBarProps) => {
|
||||
/** 초기화·조건 변경 이전 시설 후보 응답이 현재 선택을 되살리지 못하게 한다. */
|
||||
const facilityRequestRef = useRef(0);
|
||||
/** 라벨 변경은 선택을 지우지 않고 새 revision에서만 초기화한다. */
|
||||
const appliedResetRevisionRef = useRef<number | undefined>(undefined);
|
||||
const [sidoValue, setSidoValue] = useState("0");
|
||||
const [sigunValue, setSigunValue] = useState("0");
|
||||
const [sidoData, setSidoData] = useState<CenterSelectDataType[]>([
|
||||
@@ -157,6 +167,7 @@ export const ProcSearchBar = ({
|
||||
sidoNm: string,
|
||||
currentFacilityType?: string,
|
||||
) => {
|
||||
const request = ++facilityRequestRef.current;
|
||||
const type = currentFacilityType ?? facilityType;
|
||||
if (!type || type === "0") return;
|
||||
|
||||
@@ -240,6 +251,7 @@ export const ProcSearchBar = ({
|
||||
}));
|
||||
}
|
||||
|
||||
if (request !== facilityRequestRef.current) return;
|
||||
const sorted = list
|
||||
.slice()
|
||||
.sort((a, b) => a.text.localeCompare(b.text, "ko"));
|
||||
@@ -248,6 +260,20 @@ export const ProcSearchBar = ({
|
||||
},
|
||||
[defaultFacilityData, facilityLayerFilter, facilityType],
|
||||
);
|
||||
/** 외부 이동은 검색값만 초기화하고 지도·레이어 및 원천 지역 목록은 유지한다. */
|
||||
useLayoutEffect(() => {
|
||||
if (resetRevision === undefined || appliedResetRevisionRef.current === resetRevision) return;
|
||||
appliedResetRevisionRef.current = resetRevision;
|
||||
facilityRequestRef.current += 1;
|
||||
setSidoValue("0");
|
||||
setSigunValue("0");
|
||||
setFacilityValue("0");
|
||||
setFilteredSigunData([{ text: "시군구", val: "0" }]);
|
||||
setFacilityData(defaultFacilityData());
|
||||
setPendingExternalFacilityName("");
|
||||
}, [resetRevision, defaultFacilityData]);
|
||||
|
||||
useEffect(() => () => { facilityRequestRef.current += 1; }, []);
|
||||
useEffect(() => {
|
||||
fetchSido();
|
||||
fetchSigungu();
|
||||
@@ -262,6 +288,7 @@ export const ProcSearchBar = ({
|
||||
return;
|
||||
}
|
||||
|
||||
facilityRequestRef.current += 1;
|
||||
setFacilityData(defaultFacilityData());
|
||||
setFacilityValue("0");
|
||||
}, [
|
||||
@@ -317,6 +344,8 @@ export const ProcSearchBar = ({
|
||||
|
||||
const onSelectValue = useCallback(
|
||||
(val: string, type: string) => {
|
||||
facilityRequestRef.current += 1;
|
||||
onSearchInteraction?.();
|
||||
if (type === "ctpvCd") {
|
||||
setSidoValue(val);
|
||||
initializeSigunData(val);
|
||||
@@ -368,10 +397,12 @@ export const ProcSearchBar = ({
|
||||
sidoData,
|
||||
sidoValue,
|
||||
facilityType,
|
||||
onSearchInteraction,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
onSearchInteraction?.();
|
||||
if (sidoValue === "0") {
|
||||
showBusinessUserMessage("시도를 선택해 주세요.");
|
||||
return;
|
||||
@@ -400,6 +431,7 @@ export const ProcSearchBar = ({
|
||||
filteredSigunData,
|
||||
sigunData,
|
||||
sidoSearchZoom,
|
||||
onSearchInteraction,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -79,6 +79,8 @@ export const moveMapToCenter = (
|
||||
gisMaps: RefObject<any>,
|
||||
centerCoord?: [number, number],
|
||||
zoom?: number,
|
||||
/** 유효한 중심을 확보한 뒤 소비자가 이동을 승인/취소하는 선택적 callback이다. */
|
||||
beforeMove?: (extent: number[]) => boolean,
|
||||
) => {
|
||||
if (!centerCoord) return;
|
||||
|
||||
@@ -99,6 +101,7 @@ export const moveMapToCenter = (
|
||||
}
|
||||
}
|
||||
|
||||
if (beforeMove && !beforeMove([center[0], center[1], center[0], center[1]])) return;
|
||||
view.animate({
|
||||
center,
|
||||
...(typeof zoom === "number" ? { zoom } : {}),
|
||||
|
||||
Reference in New Issue
Block a user