From 823d1d7812c58a85d1f916442388a73840fe3fe7 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Tue, 9 Jun 2026 16:42:15 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=EB=B0=98=EC=B9=A8=ED=95=98=20?= =?UTF-8?q?=EC=97=B0=EB=8F=84=20=EC=A1=B0=EA=B1=B4=EA=B3=BC=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EC=A1=B0=ED=9A=8C=EC=A1=B0=EA=B1=B4=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/sinkhole/SinkholePopupPage.tsx | 11 +++++-- .../map/components/sinkhole/sinkholeTypes.ts | 32 +++++++++++++++---- apps/sn/app/(map-layout)/map/page.tsx | 30 +++++++++++++---- .../002-qa-SFR-010-010-지반침하정보-통합검증결과.md | 23 +++++++++++-- .../001-fe-SFR-010-010-지반침하정보-통합구현기록.md | 29 +++++++++++++++++ 5 files changed, 106 insertions(+), 19 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx index a8db43e10..b5bdecd7b 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholePopupPage.tsx @@ -93,6 +93,13 @@ const unwrapPayload = (response: any): T => export default function SinkholePopupPage() { const searchParams = useSearchParams(); + const hasInitialFilterParams = + searchParams.has("startYmd") || + searchParams.has("endYmd") || + searchParams.has("ctpvCd") || + searchParams.has("sggCd") || + searchParams.has("restoreStatus") || + searchParams.has("coordStatus"); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); const [filters, setFilters] = useState(() => { const seeded = normalizeSinkholeFilters( @@ -107,7 +114,7 @@ export default function SinkholePopupPage() { undefined, ); - if (!seeded.startYmd || !seeded.endYmd) { + if (!hasInitialFilterParams) { return normalizeSinkholeFilters(buildSinkholeDefaultFilters(), undefined); } return seeded; @@ -125,7 +132,7 @@ export default function SinkholePopupPage() { undefined, ); - if (!seeded.startYmd || !seeded.endYmd) { + if (!hasInitialFilterParams) { return normalizeSinkholeFilters(buildSinkholeDefaultFilters(), undefined); } return seeded; diff --git a/apps/sn/app/(map-layout)/map/components/sinkhole/sinkholeTypes.ts b/apps/sn/app/(map-layout)/map/components/sinkhole/sinkholeTypes.ts index b2413a6f3..9ad90188e 100644 --- a/apps/sn/app/(map-layout)/map/components/sinkhole/sinkholeTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/sinkhole/sinkholeTypes.ts @@ -105,6 +105,15 @@ export const buildSinkholeDefaultFilters = (scope?: SinkholeScope): SinkholeFilt }; }; +export const buildSinkholeTreeBaseFilters = (scope?: SinkholeScope): SinkholeFilters => ({ + startYmd: "", + endYmd: "", + ctpvCd: scope?.ctpvCd || "0", + sggCd: scope?.sggCd || "0", + restoreStatus: "0", + coordStatus: "0", +}); + export const buildSinkholeScope = (userInfo: IUserInfo | null): SinkholeScope => { const admin = userInfo?.adminRoleYn === "Y"; if (admin) { @@ -394,11 +403,7 @@ export const applySinkholeYearFilters = (filters: SinkholeFilters, checkedNodesD .filter(Boolean); if (selectedYears.includes("ALL")) { - return { - ...filters, - startYmd: "", - endYmd: "", - }; + return filters; } if (selectedYears.length === 0) { @@ -414,10 +419,23 @@ export const applySinkholeYearFilters = (filters: SinkholeFilters, checkedNodesD return filters; } + const yearStart = `${numericYears[0]}0101`; + const yearEnd = `${numericYears[numericYears.length - 1]}1231`; + const nextStart = filters.startYmd ? (filters.startYmd > yearStart ? filters.startYmd : yearStart) : yearStart; + const nextEnd = filters.endYmd ? (filters.endYmd < yearEnd ? filters.endYmd : yearEnd) : yearEnd; + + if (nextStart > nextEnd) { + return { + ...filters, + startYmd: "99991231", + endYmd: "99991231", + }; + } + return { ...filters, - startYmd: `${numericYears[0]}0101`, - endYmd: `${numericYears[numericYears.length - 1]}1231`, + startYmd: nextStart, + endYmd: nextEnd, }; }; diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index fa9305475..bf433650e 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -82,8 +82,10 @@ import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel"; import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel"; import { applySinkholeNodeFilters, + applySinkholeYearFilters, buildSinkholeYearNodes, buildSinkholeDefaultFilters, + buildSinkholeTreeBaseFilters, resolveSinkholeLegendIconName, buildSinkholeListQueryString, buildSinkholeScope, @@ -2145,6 +2147,7 @@ const appendSinkholeControlNode = (data: any[]) => { ...item, gobjTy: "", iconNm: resolveSinkholeLegendIconName(item) ?? item?.iconNm ?? null, + initEprssYn: "Y", }; } return item; @@ -2246,6 +2249,7 @@ const Map = () => { const [appliedSinkholeFilters, setAppliedSinkholeFilters] = useState(() => buildSinkholeDefaultFilters(sinkholeScope), ); + const [isSinkholePopupFilterActive, setIsSinkholePopupFilterActive] = useState(false); const [sinkholeRadiusMeters, setSinkholeRadiusMeters] = useState(SINKHOLE_DEFAULT_RADIUS); const [selectedSinkholeDetail, setSelectedSinkholeDetail] = useState(null); const [selectedSinkholeNearbyPipes, setSelectedSinkholeNearbyPipes] = useState([]); @@ -3854,8 +3858,8 @@ const Map = () => { }, []); const buildSinkholeQueryString = useCallback( - (filters: SinkholeFilters, page = 0, size = SINKHOLE_DEFAULT_POINT_LIMIT) => - buildSinkholeListQueryString(filters, ctpv, sgg, page, size), + (filters: SinkholeFilters, page = 0, size = SINKHOLE_DEFAULT_POINT_LIMIT, checkedNodesData = checkedNodesDataRef.current) => + buildSinkholeListQueryString(applySinkholeYearFilters(filters, checkedNodesData), ctpv, sgg, page, size), [ctpv, sgg], ); @@ -3866,9 +3870,13 @@ const Map = () => { const source = ensureSinkholeLayer(); if (!source) return; - const normalized = normalizeSinkholeFilters(filters, sinkholeScope); + const popupFilters = normalizeSinkholeFilters(filters, sinkholeScope); + const treeBaseFilters = normalizeSinkholeFilters(buildSinkholeTreeBaseFilters(sinkholeScope), sinkholeScope); + const effectiveFilters = isSinkholePopupFilterActive + ? applySinkholeYearFilters(popupFilters, checkedNodesDataRef.current) + : applySinkholeYearFilters(treeBaseFilters, checkedNodesDataRef.current); try { - const params = new URLSearchParams(buildSinkholeQueryString(normalized, 0, SINKHOLE_DEFAULT_POINT_LIMIT)); + const params = new URLSearchParams(buildSinkholeQueryString(effectiveFilters, 0, SINKHOLE_DEFAULT_POINT_LIMIT)); const rawResponse = await getRawJson( `/sewerage-service/api/v1/sinkholes?${params.toString()}`, ); @@ -3903,7 +3911,7 @@ const Map = () => { source.clear(); } }, - [appliedSinkholeFilters, buildSinkholeQueryString, ensureSinkholeLayer, isSinkholeChecked, mapReady, sinkholeScope], + [appliedSinkholeFilters, buildSinkholeQueryString, ensureSinkholeLayer, isSinkholeChecked, isSinkholePopupFilterActive, mapReady, sinkholeScope], ); const closeSinkholeDetail = useCallback(() => { @@ -4148,12 +4156,17 @@ const Map = () => { const openSinkholePopupWindow = useCallback(() => { const normalized = normalizeSinkholeFilters(appliedSinkholeFilters, sinkholeScope); - const popup = openWindowPopup(SINKHOLE_POPUP_KEY as WindowPopupKey, normalized as Record); + const effectiveFilters = applySinkholeYearFilters(normalized, checkedNodesDataRef.current); + const popup = openWindowPopup(SINKHOLE_POPUP_KEY as WindowPopupKey, effectiveFilters as Record); if (!popup) { alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요."); return null; } + setIsSinkholePopupFilterActive(true); + setAppliedSinkholeFilters(effectiveFilters); + setSinkholeSelectionVersion((version) => version + 1); + if (sinkholePopupCloseWatcherRef.current) { window.clearInterval(sinkholePopupCloseWatcherRef.current); sinkholePopupCloseWatcherRef.current = null; @@ -4166,6 +4179,7 @@ const Map = () => { sinkholePopupCloseWatcherRef.current = null; } sinkholePopupWindowRef.current = null; + setIsSinkholePopupFilterActive(false); const defaultFilters = normalizeSinkholeFilters(buildSinkholeDefaultFilters(sinkholeScope), sinkholeScope); setAppliedSinkholeFilters(defaultFilters); setSinkholeSelectionVersion((version) => version + 1); @@ -4289,6 +4303,7 @@ const Map = () => { if (event.data?.type === SINKHOLE_FILTER_SYNC_MESSAGE && event.data?.filters) { const normalized = normalizeSinkholeFilters(event.data.filters as SinkholeFilters, sinkholeScope); + setIsSinkholePopupFilterActive(true); setAppliedSinkholeFilters(normalized); setSinkholeSelectionVersion((version) => version + 1); return; @@ -6095,10 +6110,11 @@ const Map = () => { } const normalized = normalizeSinkholeFilters(appliedSinkholeFilters, sinkholeScope); + const effectiveFilters = applySinkholeYearFilters(normalized, checkedNodesDataRef.current); popup.postMessage( { type: SINKHOLE_FILTER_SYNC_MESSAGE, - filters: normalized, + filters: effectiveFilters, }, window.location.origin, ); diff --git a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md index 1d6c35564..9ccebd00b 100644 --- a/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-010-지반침하정보/002-qa-SFR-010-010-지반침하정보-통합검증결과.md @@ -74,6 +74,22 @@ - `좌표보정상태` 옵션에서 `주소없음`은 제거되고 `전체 / 성공 / 실패`만 남는 코드 경로를 확인했다. - 지반침하 다운로드는 `headerRange=A1:I1`로 수정돼 첫 데이터 행이 헤더 스타일을 먹지 않도록 정리된 것을 확인했다. +### 2.11 2026-06-09 팝업 활성 중 필터 우선순위 코드 경로 확인 + +- 평소 지도 레이어 조회는 트리 연도 체크 상태를 기준으로 하고, 팝업 sync 메시지를 받은 이후에만 팝업 필터가 우선 적용되도록 `isSinkholePopupFilterActive` 분기가 추가된 것을 확인했다. +- 팝업 close watcher와 레이어 해제 경로에서도 popup-active 상태를 해제하도록 맞춰, 팝업 종료 후 다시 트리 기준으로 복귀하는 코드 경로를 확인했다. + +### 2.12 2026-06-09 기능 테스트 최종 확인 + +- 사용자 확인 기준으로 아래 주요 흐름이 기대대로 동작함을 최종 확인했다. + - 지반침하 트리 기본 체크 상태 유지 + - `2026년` / `전체` 상위조건과 팝업 조회조건의 결합 + - 팝업 `목록보기` 오픈 시 지도 즉시 동기화 + - 팝업 닫힘 시 트리 기준 복귀 + - 팝업 열린 상태에서 레이어 재체크 시 조건 유지 + - 다운로드 첫 데이터 행 헤더 스타일 오염 제거 +- 현재 대화 범위에서는 기능 테스트 성공으로 판정한다. + ## 3. 미실행 항목 - FE 브라우저 검증 @@ -92,9 +108,10 @@ ## 4. 현재 상태 판정 -- 설계 검토: 진행 -- 코드 구현: 미착수 -- 브라우저/API 검증: 미실행 +- 설계 검토: 완료 +- 코드 구현: 완료 +- 기능 테스트: 성공 +- 브라우저/API 정밀 재검증: 일부 미실행 ## 5. 후속 실행 조건 diff --git a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md index a47efd727..9ef180d58 100644 --- a/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-010-지반침하정보/001-fe-SFR-010-010-지반침하정보-통합구현기록.md @@ -165,6 +165,35 @@ - 그래서 지도 레이어 조회와 팝업 초기 전달에는 연도 노드 덮어쓰기를 제거하고, 팝업 조회조건 자체를 유일한 필터 기준으로 정리했다. - 결과적으로 초기 로드, 팝업 조회 후 지도 반영, 팝업 닫힘 후 기본 최근 1년 복귀가 같은 필터 흐름으로 맞춰졌다. +### 4.22 2026-06-09 팝업 활성 중 필터 우선순위 재정리 + +- 추가 확인 결과 사용자가 원하는 동작은 “평소에는 트리 연도 체크 상태 기준, 팝업이 열려 조회를 수행한 동안만 팝업 조건 우선, 팝업을 닫으면 다시 트리 기준 복귀”였다. +- 그래서 지반침하 레이어 조회에 `isSinkholePopupFilterActive` 상태를 추가하고, 팝업 sync 메시지를 받았을 때만 팝업 필터를 우선 사용하도록 조정했다. +- 팝업이 닫히거나 지반침하 레이어 자체를 끄면 popup-active 상태를 해제하고, 기본 최근 1년 필터를 기준으로 다시 트리 연도 조건이 적용되도록 정리했다. + +### 4.23 2026-06-09 목록보기 오픈 즉시 지도 동기화 + +- 추가 요구사항으로 `목록보기` 버튼을 눌러 팝업이 열리는 순간에도, 팝업이 들고 뜨는 초기 조회조건이 즉시 지도에 반영돼야 한다는 기준을 확인했다. +- 그래서 `openSinkholePopupWindow()`에서 팝업 오픈 직후 `isSinkholePopupFilterActive=true`, `appliedSinkholeFilters=effectiveFilters`를 바로 반영하고 selection version도 증가시키도록 연결했다. +- 이로써 팝업 내부 첫 조회 응답을 기다리지 않아도 지도와 팝업의 초기 조건이 먼저 맞춰진다. + +### 4.24 2026-06-09 트리 상위조건 + 팝업 하위조건 결합 + +- 추가 정리 결과 최종 UX는 `2026년`, `전체`를 상위 큰 조건으로 두고, 팝업 조회조건은 그 범위 안에서만 한 번 더 좁히는 구조가 가장 자연스럽다고 판단했다. +- 그래서 popup-active가 아닐 때는 `buildSinkholeTreeBaseFilters()`에 트리 연도 조건만 적용하고, popup-active일 때만 `팝업 조회조건 ∩ 트리 연도조건`을 계산하도록 로직을 바꿨다. +- 이로써 `2026년`만 체크 시 다른 연도는 나오지 않고, `전체` 체크 시에는 팝업 조건이 전체 범위 안에서만 적용되며, 팝업 종료 후에는 다시 트리 기준으로 복귀한다. + +### 4.25 2026-06-09 팝업 열린 상태에서 레이어 재체크 시 필터 유지 + +- 추가 확인 결과 팝업이 열린 상태에서 지반침하 레이어를 껐다가 다시 켜면, 레이어 해제 분기에서 popup-active 상태와 `appliedSinkholeFilters`를 같이 초기화하고 있어 팝업 조건이 사라지고 있었다. +- 이건 팝업 close 시점과 레이어 토글 시점을 혼동한 것이므로, 레이어 해제 시에는 popup-active와 팝업 필터를 건드리지 않고 레이어/상세만 닫도록 수정했다. +- 이제 팝업이 열린 상태에서 레이어를 다시 켜면 방금 보던 팝업 조건이 그대로 유지된 채 지도에 재적용된다. + +### 4.26 2026-06-09 기능 테스트 성공 반영 + +- 최종 사용자 확인 기준으로 현재 대화에서 다뤘던 지반침하 팝업/지도/다운로드 연동 이슈는 모두 기대 동작으로 수렴했다. +- 따라서 본 문서는 이번 라운드 기능 구현이 사용자 기능 테스트 기준 성공 상태임을 기준점으로 남긴다. + ### 4.16 2026-06-09 연도 노드 아이콘 외곽선 재조정 - 연도 노드 아이콘은 단색 점만 두었을 때 경계가 약해 보여, 사용자가 보여준 예시처럼 `색 원 + 흰 외곽선` 형태가 더 읽기 쉽다고 판단했다.