지반침하 연도 조건과 팝업 조회조건 동기화 정리
This commit is contained in:
@@ -93,6 +93,13 @@ const unwrapPayload = <T,>(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<SinkholeFilters>(() => {
|
||||
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;
|
||||
|
||||
@@ -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: `${numericYears[0]}0101`,
|
||||
endYmd: `${numericYears[numericYears.length - 1]}1231`,
|
||||
startYmd: "99991231",
|
||||
endYmd: "99991231",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...filters,
|
||||
startYmd: nextStart,
|
||||
endYmd: nextEnd,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<SinkholeFilters>(() =>
|
||||
buildSinkholeDefaultFilters(sinkholeScope),
|
||||
);
|
||||
const [isSinkholePopupFilterActive, setIsSinkholePopupFilterActive] = useState(false);
|
||||
const [sinkholeRadiusMeters, setSinkholeRadiusMeters] = useState(SINKHOLE_DEFAULT_RADIUS);
|
||||
const [selectedSinkholeDetail, setSelectedSinkholeDetail] = useState<SinkholeDetail | null>(null);
|
||||
const [selectedSinkholeNearbyPipes, setSelectedSinkholeNearbyPipes] = useState<SinkholeNearbyPipe[]>([]);
|
||||
@@ -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<string, string>);
|
||||
const effectiveFilters = applySinkholeYearFilters(normalized, checkedNodesDataRef.current);
|
||||
const popup = openWindowPopup(SINKHOLE_POPUP_KEY as WindowPopupKey, effectiveFilters as Record<string, string>);
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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. 후속 실행 조건
|
||||
|
||||
|
||||
@@ -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 연도 노드 아이콘 외곽선 재조정
|
||||
|
||||
- 연도 노드 아이콘은 단색 점만 두었을 때 경계가 약해 보여, 사용자가 보여준 예시처럼 `색 원 + 흰 외곽선` 형태가 더 읽기 쉽다고 판단했다.
|
||||
|
||||
Reference in New Issue
Block a user