노후관로 레이어를 DB 설정 기반으로 전환
This commit is contained in:
@@ -14,7 +14,6 @@ import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_LIST_HEADLIST,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
buildAgedPipeCqlFilters,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
type AgedPipeManagementAgency,
|
||||
} from "./agedPipeUtils";
|
||||
import { downloadAgedPipeExcel } from "./agedPipeExport";
|
||||
import { fetchAgedPipeLayer } from "./agedPipeLayer";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
|
||||
@@ -100,12 +100,14 @@ export default function AgedPipePopupPage() {
|
||||
const fetchRows = useCallback(
|
||||
async (page: number, nextFilters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(nextFilters);
|
||||
// 독립 팝업에서도 현재 DB의 원천 레이어와 필터 템플릿을 사용한다.
|
||||
const layer = await fetchAgedPipeLayer();
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
lyrTypeNm: layer.lyrTyNm,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
});
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized, layer.flterSql);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
@@ -139,13 +141,14 @@ export default function AgedPipePopupPage() {
|
||||
setAppliedFilters(normalizeAgedPipeFilters(nextFilters));
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
alert(error instanceof Error ? error.message : "노후관로 목록 조회 중 오류가 발생했습니다.");
|
||||
setRows([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchRows, filters],
|
||||
[alert, fetchRows, filters],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/** 노후관로의 지도와 목록이 사용하는 DB 레이어 설정 계약이다. */
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { buildAgedPipeCqlFilters, isAgedPipeLayerNode, type AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
/** DB 응답을 그대로 보존하며 최초 표출 전에 필터만 해석한다. */
|
||||
export const prepareAgedPipeLayerRows = (rows: any[], filters: AgedPipeFilters): any[] =>
|
||||
rows.map((row) => {
|
||||
const next = { ...row };
|
||||
if (isAgedPipeLayerNode(row)) {
|
||||
next.agedPipeFilterTemplate = row.flterSql;
|
||||
try {
|
||||
validateAgedPipeLayer(row);
|
||||
next.flterSql = buildAgedPipeCqlFilters(filters, row.flterSql).join(" AND ");
|
||||
} catch (error) {
|
||||
// 노후관로 DB 미전환이 다른 시설물 레이어까지 중단시키지 않도록 이 행만 차단한다.
|
||||
next.agedPipeConfigError = error instanceof Error ? error.message : "노후관로 레이어 설정 오류";
|
||||
next.actlLyrYn = "N";
|
||||
next.flterSql = "EXCLUDE";
|
||||
}
|
||||
}
|
||||
if (Array.isArray(row.children)) next.children = prepareAgedPipeLayerRows(row.children, filters);
|
||||
return next;
|
||||
});
|
||||
|
||||
/** 환경마다 달라질 수 있는 일련번호 대신 DB 업무명을 통해 설정 행을 찾는다. */
|
||||
export const findAgedPipeLayer = (rows: any[]): any | undefined => {
|
||||
for (const row of rows) {
|
||||
if (isAgedPipeLayerNode(row)) return row;
|
||||
const child = Array.isArray(row.children) ? findAgedPipeLayer(row.children) : undefined;
|
||||
if (child) return child;
|
||||
}
|
||||
};
|
||||
|
||||
/** 예전 제어용 행을 실제 WFS 레이어처럼 요청하지 않도록 전환 상태를 확인한다. */
|
||||
export const validateAgedPipeLayer = (row: any) => {
|
||||
if (row?.agedPipeConfigError) throw new Error(row.agedPipeConfigError);
|
||||
if (!row || row.actlLyrYn !== "Y" || row.srvcNm !== "wfs" || !row.lyrTyNm || row.lyrTyNm === "tb_old_pipe") {
|
||||
throw new Error("노후관로 DB 레이어 설정을 실제 WFS 레이어와 sql_filt 템플릿으로 갱신해 주세요.");
|
||||
}
|
||||
};
|
||||
|
||||
/** 독립 목록 팝업도 부모창의 하드코딩 없이 동일 설정 API를 조회한다. */
|
||||
export const fetchAgedPipeLayer = async () => {
|
||||
const rows = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
const row = findAgedPipeLayer(Array.isArray(rows) ? rows : []);
|
||||
validateAgedPipeLayer(row);
|
||||
return row;
|
||||
};
|
||||
@@ -229,13 +229,18 @@ export const getElapsedYearsDisplayText = (value: unknown) => {
|
||||
return elapsedYears == null ? "확인불가" : `${elapsedYears}년`;
|
||||
};
|
||||
|
||||
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => {
|
||||
/** DB의 SQL 필터 템플릿을 해석한다. 누락/미지원 토큰은 전체 관로 조회로 대체하지 않는다. */
|
||||
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters, filterTemplate: string) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const cqlFilters = [
|
||||
"instl_ymd IS NOT NULL",
|
||||
...AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.map((prefix) => `instl_ymd NOT LIKE '${prefix}%'`),
|
||||
`instl_ymd <= '${getCutoffYmd(normalized.elapsedYears)}'`,
|
||||
];
|
||||
const template = String(filterTemplate ?? "").trim();
|
||||
if (!template.includes("${cutoffYmd}")) {
|
||||
throw new Error("노후관로 DB sql_filt에 ${cutoffYmd} 조건을 등록해 주세요.");
|
||||
}
|
||||
const filter = template.split("${cutoffYmd}").join(getCutoffYmd(normalized.elapsedYears));
|
||||
if (/\$\{[^}]*\}/.test(filter)) {
|
||||
throw new Error("노후관로 DB sql_filt에 지원하지 않는 치환값이 있습니다.");
|
||||
}
|
||||
const cqlFilters = [`(${filter})`];
|
||||
|
||||
if (normalized.sggCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.sggCd}%'`);
|
||||
|
||||
@@ -110,7 +110,6 @@ import {
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_GUIDE_MIN_ZOOM,
|
||||
AGED_PIPE_MIN_ZOOM,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
type AgedPipeFilters,
|
||||
@@ -121,6 +120,7 @@ import {
|
||||
toDisplayPipeRow,
|
||||
validateElapsedYears,
|
||||
} from "./components/agedPipe/agedPipeUtils";
|
||||
import { findAgedPipeLayer, prepareAgedPipeLayerRows, validateAgedPipeLayer } from "./components/agedPipe/agedPipeLayer";
|
||||
import { downloadAgedPipeExcel } from "./components/agedPipe/agedPipeExport";
|
||||
import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap";
|
||||
import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap";
|
||||
@@ -218,10 +218,6 @@ const AnalysisPopupMenu = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
// 노후관로 지도 강조선과 시설물 트리의 선형 범례가 동일하게 사용하는 빨간색이다.
|
||||
const AGED_PIPE_HIGHLIGHT_COLOR = "rgba(235, 40, 40, 0.82)";
|
||||
const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS";
|
||||
// 심선은 동일 레이어 유형에 6개 깊이 등급 설정이 있어 prefix 뒤에 lyrSn을 붙여 각각 생성한다.
|
||||
const FLDNG_DEPTH_WFS_LAYER_ID_PREFIX = "SN_FLDNG_DEPTH_WFS";
|
||||
@@ -979,7 +975,7 @@ const extractCheckedPipeUsageStyles = (
|
||||
|
||||
checkedNodesData.forEach((node) => {
|
||||
const data = node?.data ?? node;
|
||||
if (!data || isFlowDirectionLayerInfo(data)) return;
|
||||
if (!data || isFlowDirectionLayerInfo(data) || isAgedPipeLayerNode(data)) return;
|
||||
if (getLayerTypeName(data) !== FLOW_DIRECTION_PIPE_LAYER_TYPE) return;
|
||||
if (!isLayerVisibleAtZoom(data, zoom)) return;
|
||||
|
||||
@@ -1000,61 +996,7 @@ const extractCheckedPipeUsageStyles = (
|
||||
return usageStyleMap;
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const normalizeTreeRowSpacing = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
@@ -2310,14 +2252,16 @@ const Map = () => {
|
||||
const data = await getRawJson(
|
||||
"/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK",
|
||||
);
|
||||
// DB 필터 치환 실패는 노후관로에만 한정하고 다른 시설물 레이어는 유지한다.
|
||||
const preparedData = prepareAgedPipeLayerRows(data, appliedAgedPipeFilters);
|
||||
const agedPipeConfigError = findAgedPipeLayer(preparedData)?.agedPipeConfigError;
|
||||
if (agedPipeConfigError) alert(agedPipeConfigError);
|
||||
setTreeData(
|
||||
appendExmnResultControlNodes(
|
||||
appendAgedPipeControlNode(
|
||||
appendSinkholeControlNode(
|
||||
normalizeTreeRowSpacing(
|
||||
normalizeRainObservationLayerInteraction(
|
||||
normalizeFlowDirectionLayerInfos(data),
|
||||
),
|
||||
appendSinkholeControlNode(
|
||||
normalizeTreeRowSpacing(
|
||||
normalizeRainObservationLayerInteraction(
|
||||
normalizeFlowDirectionLayerInfos(preparedData),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -3204,14 +3148,16 @@ const Map = () => {
|
||||
const fetchAgedPipeRows = useCallback(
|
||||
async (page: number, filters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const layer = findAgedPipeLayer(treeData);
|
||||
validateAgedPipeLayer(layer);
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
lyrTypeNm: layer.lyrTyNm,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized, layer.agedPipeFilterTemplate ?? layer.flterSql);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
@@ -3226,7 +3172,7 @@ const Map = () => {
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
[ctpv, sgg, treeData],
|
||||
);
|
||||
|
||||
const loadAgedPipeList = useCallback(
|
||||
@@ -5176,54 +5122,39 @@ const Map = () => {
|
||||
restoreActiveLayerSettingScroll,
|
||||
]);
|
||||
|
||||
// DB 레이어 ID를 유지하고 조건 변경 시 해당 레이어만 갱신한다.
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const row = findAgedPipeLayer(treeData);
|
||||
if (!row) return;
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
|
||||
if (!isAgedPipeChecked || !isAgedPipeSettingEnabled) return;
|
||||
|
||||
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) =>
|
||||
item?.lyrTyNm === AGED_PIPE_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
) as any;
|
||||
|
||||
if (!baseLayerCode) return;
|
||||
|
||||
const layerCode = {
|
||||
...baseLayerCode,
|
||||
id: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrSn: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrNm: "노후관로",
|
||||
lyrTyNm: AGED_PIPE_LAYER_TYPE,
|
||||
gobjTy: "line",
|
||||
actlLyrYn: "Y",
|
||||
exprsYn: "Y",
|
||||
clickPsblYn: "Y",
|
||||
cmncNm: "bbox",
|
||||
srvcNm: "wfs",
|
||||
flterSql: buildAgedPipeCqlFilters(appliedAgedPipeFilters).join(" and "),
|
||||
lnWdth: "4",
|
||||
lnColor: AGED_PIPE_HIGHLIGHT_COLOR,
|
||||
drawOrdr: "9999",
|
||||
minViewSize: String(AGED_PIPE_MIN_ZOOM),
|
||||
};
|
||||
|
||||
const layer = core?.addWfsLayer?.(layerCode);
|
||||
layer?.set?.("id", AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
layer?.set?.("lyrNm", "노후관로");
|
||||
layer?.set?.("lyrTyNm", AGED_PIPE_LAYER_TYPE);
|
||||
|
||||
return () => {
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
};
|
||||
}, [
|
||||
appliedAgedPipeFilters,
|
||||
isAgedPipeChecked,
|
||||
isAgedPipeSettingEnabled,
|
||||
mapReady,
|
||||
]);
|
||||
const id = String(row.lyrSn);
|
||||
const layerCode = (GisApp.layerCode as any)?.[id];
|
||||
if (!layerCode) return;
|
||||
try {
|
||||
validateAgedPipeLayer(row);
|
||||
const filter = buildAgedPipeCqlFilters(
|
||||
appliedAgedPipeFilters, row.agedPipeFilterTemplate ?? row.flterSql,
|
||||
).join(" AND ");
|
||||
layerCode.flterSql = filter;
|
||||
const visible = isAgedPipeChecked && isAgedPipeSettingEnabled;
|
||||
let layer = core?.getLayerById?.(id);
|
||||
if (!visible) {
|
||||
layer?.setVisible?.(false);
|
||||
return;
|
||||
}
|
||||
// 숨긴 동안 바뀐 조건도 다시 켤 때 반영하도록 실제 레이어의 생성 당시 필터와 비교한다.
|
||||
if (!layer || layer.get?.("flterSql") !== filter) {
|
||||
layer = core?.addWfsLayer?.(layerCode);
|
||||
core?.setSelectInteraction?.();
|
||||
}
|
||||
layer?.setVisible?.(true);
|
||||
} catch (error) {
|
||||
core?.getLayerById?.(id)?.setVisible?.(false);
|
||||
console.error("노후관로 DB 레이어 설정 오류:", error);
|
||||
}
|
||||
}, [appliedAgedPipeFilters, isAgedPipeChecked, isAgedPipeSettingEnabled,
|
||||
mapReady, treeData, checkedLayerLegendVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
@@ -75,3 +75,11 @@
|
||||
| 서버 노드 호환 | 정적 통과 | 기존 노드와 프론트 가상 노드를 동일 기준으로 정규화 |
|
||||
| 변경 공백 검사 | 통과 | 프론트 저장소 `git diff --check` |
|
||||
| 브라우저 육안 확인 | 대기 | 지도 새로고침 후 체크박스 옆 빨간 선형 범례 확인 필요 |
|
||||
|
||||
## 2026-10-01 DB 레이어 설정 이관 검증
|
||||
- TypeScript 검사 통과: node node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false. 의존성 읽기 권한으로 상승 실행했다.
|
||||
- 메모리 내 자동 검증 9건 통과: DB ID/스타일/원본 템플릿 보존, 가상 행 미생성, 경과연수 변경, 시군구 우선, 시도 조건, 빈/미지원 템플릿 거부, 기존 제어 행 오류 격리, 중첩 행, SQL 템플릿 일치.
|
||||
- git diff --check 통과. 테스트 파일이나 원시 결과 파일은 생성하지 않았다.
|
||||
- 실행 중 3002 브라우저에서 DB 미전환 안내와 기존 시설물 트리/노후관로 항목 표시를 확인했다. DB 응답이 실제 WFS 설정 요건을 충족하지 않아 정상 관로 표출·목록·Excel 실데이터 회귀는 미완료이다.
|
||||
- 사용자 서버 3002/8883은 재시작하거나 종료하지 않았다. 전체 빌드는 실행하지 않았다.
|
||||
- 후속 검증: 기존 139번에 UPDATE 적용 후 새로고침, 체크/해제, 30년→40년, 설정 숨김 중 변경 후 재표시, 지도·목록·Excel 기준 일치 확인.
|
||||
|
||||
@@ -115,3 +115,12 @@
|
||||
- 실제 로그인 지도에서 노후관로 범례와 지도 강조선의 색상 일치 여부를 육안 확인해야 한다.
|
||||
- 사용자 스크린샷 기준의 정확한 위치 체감은 실제 브라우저 수동 확인이 필요하다.
|
||||
- 최소화 후 다시 열 때 사용하는 메인 화면의 미니 카드 위치는 이번 라운드 범위에 포함하지 않았다.
|
||||
|
||||
## 2026-10-01 DB 레이어 설정 이관
|
||||
- 변경 파일: map/page.tsx, agedPipe/agedPipeUtils.ts, agedPipe/AgedPipePopupPage.tsx, 신규 agedPipe/agedPipeLayer.ts.
|
||||
- appendAgedPipeControlNode, SN_AGED_PIPE 및 SN_AGED_PIPE_HIGHLIGHT 생성, 스타일 상수 덮어쓰기를 제거했다.
|
||||
- 기존 설정 API 응답의 레이어 ID·원천·스타일을 사용하고 최초 요청 전에 sql_filt의 ${cutoffYmd}를 치환한다. 조건 변경 시 같은 ID의 레이어만 갱신한다.
|
||||
- 숨긴 상태의 조건 변경도 재표시할 때 적용한다. 노후관로를 유하방향 관종 스타일 수집에서 제외한다.
|
||||
- DB 미전환/필터 오류는 해당 행만 요청 차단하고 안내하며 다른 시설물 트리는 유지한다.
|
||||
- 독립 목록 팝업도 레이어 설정 API를 사용한다. Excel API와 위치 이동 동작은 유지한다. 백엔드/공통 GIS 코드는 변경하지 않았다.
|
||||
- DB작업.sql에 INSERT 및 기존 139번용 UPDATE를 작성했다. 실제 DB 변경은 미실행이다.
|
||||
|
||||
Reference in New Issue
Block a user