sn: map 조회 버튼 추가

This commit is contained in:
dongsilove
2026-03-12 23:37:15 +09:00
parent 4cde2a56b4
commit ea490bcdbc
@@ -1,13 +1,15 @@
"use client";
import { SelectBox, type SelectDataType } from "../../../ui";
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import { transform } from "ol/proj";
import GisApp from "../lib/gis.core";
const geoserverUrl =
"/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json";
const FACILITY_TYPE_URL_MAP: Record<string, string> = {
tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설
tb_il_swage_prcs_fclt: geoserverUrl,
};
interface SearchBarProps {
@@ -15,30 +17,125 @@ interface SearchBarProps {
facilityTypes: { text: string; val: string }[];
}
interface CenterSelectDataType extends SelectDataType {
center?: [number, number];
}
export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
const [sidoValue, setSidoValue] = useState("0");
const [sigunValue, setSigunValue] = useState("0");
const [sidoData, setSidoData] = useState<SelectDataType[]>([{ text: "시도", val: "0" }]);
const [sigunData, setSigunData] = useState<SelectDataType[]>([{ text: "시군구", val: "0" }]);
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "시군구", val: "0" }]);
const [facilityData, setFacilityData] = useState<SelectDataType[]>([{ text: "하수처리시설", val: "0" }]);
const [sidoData, setSidoData] = useState<CenterSelectDataType[]>([
{ text: "시도", val: "0" },
]);
const [sigunData, setSigunData] = useState<CenterSelectDataType[]>([
{ text: "시군구", val: "0" },
]);
const [filteredSigunData, setFilteredSigunData] = useState<CenterSelectDataType[]>([
{ text: "시군구", val: "0" },
]);
const [facilityData, setFacilityData] = useState<SelectDataType[]>([
{ text: "하수처리시설", val: "0" },
]);
const [facilityValue, setFacilityValue] = useState("0");
const [facilityType, setFacilityType] = useState(
facilityTypes?.length === 1 && facilityTypes[0]
? facilityTypes[0].val
: "0",
facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0",
);
const fetchSido = async () => {
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`;
const res = await fetch(url).then(res => res.json());
const getGeometryCenter = (geometry: any): [number, number] | null => {
if (!geometry?.coordinates) return null;
const list = res.features
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
const visit = (coords: any) => {
if (!Array.isArray(coords)) return;
if (
coords.length >= 2 &&
typeof coords[0] === "number" &&
typeof coords[1] === "number"
) {
const x = coords[0];
const y = coords[1];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
return;
}
coords.forEach(visit);
};
visit(geometry.coordinates);
if (
!Number.isFinite(minX) ||
!Number.isFinite(minY) ||
!Number.isFinite(maxX) ||
!Number.isFinite(maxY)
) {
return null;
}
return [(minX + maxX) / 2, (minY + maxY) / 2];
};
const getMapCenterCoordinate = (centerCoord: [number, number]): [number, number] => {
const [x, y] = centerCoord;
const looksLikeLonLat = Math.abs(x) <= 180 && Math.abs(y) <= 90;
const looksLikeLatLonSwapped = Math.abs(x) <= 90 && Math.abs(y) <= 180;
if (looksLikeLonLat) {
return transform(centerCoord, GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number];
}
if (looksLikeLatLonSwapped) {
return transform([y, x], GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number];
}
return centerCoord;
};
const moveMapToCenter = (centerCoord?: [number, number], zoom?: number) => {
if (!centerCoord) return;
const map = gisMaps.current;
if (!map?.getView) return;
const view = map.getView();
const center = getMapCenterCoordinate(centerCoord);
if (!Number.isFinite(center[0]) || !Number.isFinite(center[1])) return;
const projection = view.getProjection?.();
const extent = projection?.getExtent?.();
if (extent) {
const [minX, minY, maxX, maxY] = extent;
if (center[0] < minX || center[0] > maxX || center[1] < minY || center[1] > maxY) {
console.warn("중심좌표가 지도 범위를 벗어나 이동을 중단합니다.", { center });
return;
}
}
view.animate({
center,
...(typeof zoom === "number" ? { zoom } : {}),
duration: 500,
});
};
const fetchSido = async () => {
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&srsName=${GisApp.TO_PROJ}&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd,geom`;
const res = await fetch(url).then((r) => r.json());
const list = (res.features || [])
.map((f: any) => ({
text: f.properties.mctpv_korn_nm,
val: f.properties.mctpv_cd,
center: getGeometryCenter(f.geometry) ?? undefined,
}))
// .sort((a: any, b: any) => a.text.localeCompare(b.text, "ko"));
.sort((a: any, b: any) => {
// 커스텀 순서 정의
const customOrder = [
@@ -76,23 +173,25 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
// 둘 다 커스텀 순서에 없으면 기본 알파벳 순서
return a.text.localeCompare(b.text, 'ko');
});
setSidoData([{ text: "시도", val: "0" }, ...list]);
};
const fetchSigungu = async () => {
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`;
const res = await fetch(url).then(res => res.json());
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&srsName=${GisApp.TO_PROJ}&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm,geom`;
const res = await fetch(url).then((r) => r.json());
const list = res.features
const list = (res.features || [])
.map((f: any) => ({
text: f.properties.sgg_korn_nm,
val: f.properties.sgg_cd,
center: getGeometryCenter(f.geometry) ?? undefined,
}))
.sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko'));
.sort((a: any, b: any) => a.text.localeCompare(b.text, "ko"));
setSigunData(list);
};
//시설명 콤보박스 조회
const fetchFacilitiesBySggNm = async (
sggCd: string,
sggNm: string,
@@ -114,23 +213,18 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
let list: SelectDataType[] = [];
if (
type === "tb_il_swage_prcs_fclt" //하수처리시설
) {
if (type === "tb_il_swage_prcs_fclt") {
const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`;
url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`;
const res = await fetch(url);
const json = await res.json();
list = json.features
list = (json.features || [])
.map((f: any) => ({
text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
val: f.id,
}))
.sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko'));
} else if (
type === "tb_wr_bdst_ruse_fclt" // 재이용시설
) {
//재용에서 참조하는 하수처리시설 목록
.sort((a: any, b: any) => a.text.localeCompare(b.text, "ko"));
} else if (type === "tb_wr_bdst_ruse_fclt") {
const paramsRes = await fetch("/reuse-service/reuseFc/pubFc");
const paramsJson = await paramsRes.json();
const params = paramsJson.data;
@@ -148,19 +242,14 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
text: item.ruseUtztnFcltNm ?? "(이름없음)",
val: item.pbnstRuseFcltId,
}));
} else if (
type === "tb_wr_ruse_fclt_dmd_co" //수요처
) {
} else if (type === "tb_wr_ruse_fclt_dmd_co") {
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.dmdCoNm ?? "(이름없음)",
val: item.dmdInfoId,
}));
} else if (
type === "tb_wr_tmpr_dmd_co" //잠재수요처
) {
// GET 방식
} else if (type === "tb_wr_tmpr_dmd_co") {
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
@@ -168,7 +257,6 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
val: item.fctryMngSn,
}));
} else {
// GET 방식
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
@@ -177,27 +265,25 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
}));
}
// 가나다순 정렬 후 set
const sorted = list.slice().sort((a, b) => a.text.localeCompare(b.text, 'ko'));
setFacilityData([{ text: "시도", val: "0" }, ...sorted]);
const sorted = list.slice().sort((a, b) => a.text.localeCompare(b.text, "ko"));
setFacilityData([{ text: "하수처리시설", val: "0" }, ...sorted]);
setFacilityValue("0");
};
//시도, 시군구 초기 데이터 조회
useEffect(() => {
fetchSido();
fetchSigungu();
}, []);
//시군구 데이터 초기화
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
(nextSidoCd: string) => {
if (nextSidoCd === "0") {
setFilteredSigunData([{ text: "시군구", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const prefix = nextSidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]);
setSigunValue("0");
@@ -205,7 +291,6 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
[sigunData],
);
//콤보박스 선택시 연관 콤보박스 처리
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
@@ -213,54 +298,78 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
initializeSigunData(val);
setFacilityData([{ text: "하수처리시설", val: "0" }]);
setFacilityValue("0");
} else if (type === "sggCd") {
return;
}
if (type === "sggCd") {
setSigunValue(val);
const sidoNm =
sidoData.find((item) => item.val === sidoValue)?.text || "";
const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || "";
const sggNm = sigunData.find((item) => item.val === val)?.text || "";
const sggCd = sigunData.find((item) => item.val === val)?.val || "";
if (sggNm && sidoNm)
fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType);
} else if (type === "facilityType") {
if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType);
return;
}
if (type === "facilityType") {
setFacilityType(val);
setFacilityValue("0");
setFacilityData([{ text: "하수처리시설", val: "0" }]);
const sidoNm =
sidoData.find((item) => item.val === sidoValue)?.text || "";
const sggNm =
sigunData.find((item) => item.val === sigunValue)?.text || "";
const sggCd =
sigunData.find((item) => item.val === sigunValue)?.val || "";
const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || "";
const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || "";
const sggCd = sigunData.find((item) => item.val === sigunValue)?.val || "";
if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
} else if (type === "fcltCd") {
setFacilityValue(val);
return;
}
// 하수처리시설일 때 바로 handleFeatureFocus 호출
if (type === "fcltCd") {
setFacilityValue(val);
if (val !== "0") {
handleFeatureFocus({
fid: val,
layerNm: "tb_il_swage_prcs_fclt", // 레이어 이름
layerNm: "tb_il_swage_prcs_fclt",
});
}
}
},
[initializeSigunData, sigunData, sigunValue, sidoData, sidoValue],
[initializeSigunData, sigunData, sigunValue, sidoData, sidoValue, facilityType],
);
const handleSearch = useCallback(() => {
if (sidoValue === "0") {
alert("시도를 선택해 주세요");
return;
}
const selectedSido = sidoData.find((item) => item.val === sidoValue);
if (!selectedSido?.center) return;
if (sigunValue !== "0") {
const selectedSigun =
filteredSigunData.find((item) => item.val === sigunValue) ??
sigunData.find((item) => item.val === sigunValue);
if (selectedSigun?.center) {
moveMapToCenter(selectedSigun.center, 12);
return;
}
}
moveMapToCenter(selectedSido.center, 11);
}, [sidoValue, sigunValue, sidoData, filteredSigunData, sigunData]);
const handleFeatureFocus = (data: any) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
//select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature: any = null;
let featureId = data.fid;
const featureId = data.fid;
map
.getLayers()
.getArray()
@@ -271,7 +380,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
if (feature) foundFeature = feature;
}
});
// 3. 피처 선택
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
@@ -284,7 +393,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
nearest: true,
});
} else {
alert("위치정보를 찾을 수 없습니다.")
alert("위치정보를 찾을 수 없습니다.");
console.warn("geometry를 찾을 수 없습니다:", data);
}
} else {
@@ -303,9 +412,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
selectClass="rounded-lg bg-white"
label=""
selectArray={sidoData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "ctpvCd")
}
onSelectValue={(val: any) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
@@ -314,9 +421,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
selectClass="rounded-lg bg-white"
label=""
selectArray={filteredSigunData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "sggCd")
}
onSelectValue={(val: any) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<SelectBox
@@ -325,11 +430,14 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
selectClass="rounded-lg bg-white"
label=""
selectArray={facilityData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "fcltCd")
}
onSelectValue={(val: any) => onSelectValue(val.toString(), "fcltCd")}
value={facilityValue}
/>
<div className="flex flex-row space-x-2">
<Button color="primary" className="mx-2 mt-1" size="sm" onClick={handleSearch}>
조회
</Button>
</div>
</div>
</div>
</div>