지도 검색 기능

This commit is contained in:
yschoi
2025-08-04 08:16:49 +09:00
parent e2171843b2
commit 937cccb224
5 changed files with 893 additions and 156 deletions
@@ -0,0 +1,124 @@
// SearchInputText.tsx
"use client";
import { Input } from "@common/components/ui";
import { useEffect, useRef, useState } from "react";
interface SearchInputTextProps {
label?: string;
initValue?: string;
setValue: (val: string) => void;
setRtnData: (val: { bscFctNm: string; bscFctCd: string }) => void;
searchList: { bscFctNm: string; bscFctCd: string }[];
onEnter?: () => void;
className?: string;
}
export default function SearchInputText({
label,
initValue,
setValue,
setRtnData,
searchList,
onEnter,
className,
}: SearchInputTextProps) {
const [showResults, setShowResults] = useState(false);
const [text, setText] = useState(initValue ?? "");
const [highlightIndex, setHighlightIndex] = useState(-1);
const wrapperRef = useRef(null);
useEffect(() => {
setText(initValue ?? "");
}, [initValue]);
useEffect(() => {
if (searchList.length > 0) {
setShowResults(true);
} else {
setShowResults(false);
}
setHighlightIndex(-1);
}, [searchList]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
wrapperRef.current &&
!(wrapperRef.current as any).contains(event.target)
) {
setShowResults(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [wrapperRef]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
setValue(e.target.value);
setHighlightIndex(-1);
};
const handleSelect = (item: { bscFctNm: string; bscFctCd: string }) => {
setText(item.bscFctNm);
setValue(item.bscFctNm);
setRtnData(item);
setShowResults(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
if (highlightIndex >= 0 && highlightIndex < searchList.length) {
const selected = searchList[highlightIndex];
if (selected) handleSelect(selected);
} else if (onEnter) {
onEnter();
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
setHighlightIndex((prev) =>
prev < searchList.length - 1 ? prev + 1 : prev
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setHighlightIndex((prev) => (prev > 0 ? prev - 1 : -1));
}
};
return (
<div className="relative" ref={wrapperRef}>
<Input
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="시설명을 입력하세요"
className={
className + " rounded-lg border px-2 py-1 text-sm h-[29.5px]"
}
/>
{showResults && (
<div className="absolute z-10 mt-1 max-h-200 w-full overflow-auto rounded border bg-white shadow">
{searchList.length > 0 ? (
searchList.map((item, index) => (
<div
key={index}
className={`cursor-pointer px-3 py-1 hover:bg-gray-100 ${
highlightIndex === index ? "bg-blue-100" : ""
}`}
onClick={() => handleSelect(item)}
>
{item.bscFctNm}
</div>
))
) : (
<div className="px-3 py-2 text-sm text-gray-500">
검색 결과가 없습니다.
</div>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,299 @@
"use client";
import { SelectBox, type SelectDataType } from "@common/business_components/ui";
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import { SERVER_API_WR_URL } from "@/src/shared/config";
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_wr_bdst_ruse_fclt:
"/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post
tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get
tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get
tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get
tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get
tb_wr_tmpr_dmd_co:
"/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get
};
interface SearchBarProps {
onFeatureSelect: (feature: any) => void;
gisMaps: React.RefObject<any>;
facilityTypes: { text: string; val: string }[];
}
export function SearchBar({
onFeatureSelect,
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 [facilityValue, setFacilityValue] = useState("0");
const [facilityType, setFacilityType] = useState(
facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0"
);
//시도, 시군구 조회
const fetchSido = async () => {
const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`;
const res = await fetch(url);
const json = await res.json();
const sidoList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length === 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
setSidoData([{ text: "선택", val: "0" }, ...sidoList]);
const sigunList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
setSigunData(sigunList);
};
//시설명 콤보박스 조회
const fetchFacilitiesBySggNm = async (
sggCd: string,
sggNm: string,
sidoNm: string,
currentFacilityType?: string
) => {
const type = currentFacilityType ?? facilityType;
if (!type || type === "0") return;
const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
if (!baseUrlTemplate) return;
const year = new Date().getFullYear().toString();
let url = baseUrlTemplate
.replace("{year}", year)
.replace("{sggCd}", sggCd)
.replace("{sidoNm}", encodeURIComponent(sidoNm))
.replace("{sggNm}", encodeURIComponent(sggNm));
let list: SelectDataType[] = [];
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.map((f: any) => ({
text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
val: f.id,
}));
} 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;
const res = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(params),
});
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.ruseUtztnFcltNm ?? "(이름없음)",
val: item.pbnstRuseFcltId,
}));
} 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 방식
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.dmdCoNm ?? "(이름없음)",
val: item.fctryMngSn,
}));
} else {
// GET 방식
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.bdstNm ?? "(이름없음)",
val: item.bdstInfoSn,
}));
}
setFacilityData([{ text: "선택", val: "0" }, ...list]);
setFacilityValue("0");
};
//시도, 시군구 초기 데이터 조회
useEffect(() => {
fetchSido();
}, []);
//시군구 데이터 초기화
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setFilteredSigunData([{ text: "선택", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]);
setSigunValue("0");
},
[sigunData]
);
//콤보박스 선택시 연관 콤보박스 처리
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
setSidoValue(val);
initializeSigunData(val);
setFacilityData([{ text: "선택", val: "0" }]);
setFacilityValue("0");
} else if (type === "sggCd") {
setSigunValue(val);
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") {
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 || "";
if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
} else if (type === "fcltCd") {
setFacilityValue(val);
}
},
[initializeSigunData, sigunData, sigunValue, sidoData, sidoValue]
);
//조회 버튼 클릭
const handleSearch = () => {
if (!gisMaps.current || facilityValue === "0") return;
const layers = gisMaps.current.getLayers().getArray();
for (const layer of layers) {
const source = layer.getSource?.();
const feature = source?.getFeatureById?.(facilityValue);
if (feature) {
onFeatureSelect(feature);
return;
}
}
alert("해당 처리시설을 지도에서 찾을 수 없습니다.");
};
return (
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">
<div className="card card-border flex">
<div className="col-span-10 flex flex-row">
{facilityTypes.length > 1 && (
<SelectBox
labelClass="w-[120px]"
className="mr-10 w-[190px]"
selectClass="rounded-lg bg-white"
label="시설물유형"
selectArray={[{ text: "선택", val: "0" }, ...facilityTypes]}
onSelectValue={(val) =>
onSelectValue(val.toString(), "facilityType")
}
value={facilityType}
/>
)}
<SelectBox
labelClass="w-[40px]"
className="mr-10 w-[150px]"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidoData}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="mr-10 w-[150px]"
selectClass="rounded-lg bg-white"
label="시군구"
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<SelectBox
labelClass="w-[60px]"
className="mr-3 w-[180px]"
selectClass="rounded-lg bg-white"
label="시설명"
selectArray={facilityData}
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
value={facilityValue}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="primary"
className="mx-2 mt-1 ml-5"
size={"sm"}
onClick={handleSearch}
>
조회
</Button>
</div>
</div>
</div>
</div>
);
}
+299
View File
@@ -0,0 +1,299 @@
"use client";
import { SelectBox, type SelectDataType } from "@common/business_components/ui";
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import { SERVER_API_WR_URL } from "@/src/shared/config";
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_wr_bdst_ruse_fclt:
"/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post
tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get
tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get
tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get
tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get
tb_wr_tmpr_dmd_co:
"/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get
};
interface SearchBarProps {
onFeatureSelect: (feature: any) => void;
gisMaps: React.RefObject<any>;
facilityTypes: { text: string; val: string }[];
}
export function SearchBar({
onFeatureSelect,
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 [facilityValue, setFacilityValue] = useState("0");
const [facilityType, setFacilityType] = useState(
facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0"
);
//시도, 시군구 조회
const fetchSido = async () => {
const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`;
const res = await fetch(url);
const json = await res.json();
const sidoList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length === 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
setSidoData([{ text: "선택", val: "0" }, ...sidoList]);
const sigunList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
setSigunData(sigunList);
};
//시설명 콤보박스 조회
const fetchFacilitiesBySggNm = async (
sggCd: string,
sggNm: string,
sidoNm: string,
currentFacilityType?: string
) => {
const type = currentFacilityType ?? facilityType;
if (!type || type === "0") return;
const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
if (!baseUrlTemplate) return;
const year = new Date().getFullYear().toString();
let url = baseUrlTemplate
.replace("{year}", year)
.replace("{sggCd}", sggCd)
.replace("{sidoNm}", encodeURIComponent(sidoNm))
.replace("{sggNm}", encodeURIComponent(sggNm));
let list: SelectDataType[] = [];
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.map((f: any) => ({
text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
val: f.id,
}));
} 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;
const res = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(params),
});
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.ruseUtztnFcltNm ?? "(이름없음)",
val: item.pbnstRuseFcltId,
}));
} 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 방식
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.dmdCoNm ?? "(이름없음)",
val: item.fctryMngSn,
}));
} else {
// GET 방식
const res = await fetch(url);
const json = await res.json();
list = (json.data || []).map((item: any) => ({
text: item.bdstNm ?? "(이름없음)",
val: item.bdstInfoSn,
}));
}
setFacilityData([{ text: "선택", val: "0" }, ...list]);
setFacilityValue("0");
};
//시도, 시군구 초기 데이터 조회
useEffect(() => {
fetchSido();
}, []);
//시군구 데이터 초기화
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setFilteredSigunData([{ text: "선택", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]);
setSigunValue("0");
},
[sigunData]
);
//콤보박스 선택시 연관 콤보박스 처리
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
setSidoValue(val);
initializeSigunData(val);
setFacilityData([{ text: "선택", val: "0" }]);
setFacilityValue("0");
} else if (type === "sggCd") {
setSigunValue(val);
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") {
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 || "";
if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
} else if (type === "fcltCd") {
setFacilityValue(val);
}
},
[initializeSigunData, sigunData, sigunValue, sidoData, sidoValue]
);
//조회 버튼 클릭
const handleSearch = () => {
if (!gisMaps.current || facilityValue === "0") return;
const layers = gisMaps.current.getLayers().getArray();
for (const layer of layers) {
const source = layer.getSource?.();
const feature = source?.getFeatureById?.(facilityValue);
if (feature) {
onFeatureSelect(feature);
return;
}
}
alert("해당 처리시설을 지도에서 찾을 수 없습니다.");
};
return (
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">
<div className="card card-border flex">
<div className="col-span-10 flex flex-row">
{facilityTypes.length > 1 && (
<SelectBox
labelClass="w-[120px]"
className="mr-10 w-[190px]"
selectClass="rounded-lg bg-white"
label="시설물유형"
selectArray={[{ text: "선택", val: "0" }, ...facilityTypes]}
onSelectValue={(val) =>
onSelectValue(val.toString(), "facilityType")
}
value={facilityType}
/>
)}
<SelectBox
labelClass="w-[40px]"
className="mr-10 w-[150px]"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidoData}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="mr-10 w-[150px]"
selectClass="rounded-lg bg-white"
label="시군구"
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<SelectBox
labelClass="w-[60px]"
className="mr-3 w-[180px]"
selectClass="rounded-lg bg-white"
label="시설명"
selectArray={facilityData}
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
value={facilityValue}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="primary"
className="mx-2 mt-1 ml-5"
size={"sm"}
onClick={handleSearch}
>
조회
</Button>
</div>
</div>
</div>
</div>
);
}
+160 -156
View File
@@ -1,22 +1,25 @@
// SearchBar.tsx
"use client";
import { SelectBox, type SelectDataType } from "@common/business_components/ui";
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import { SERVER_API_WR_URL } from "@/src/shared/config";
const geoserverUrl =
"/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json";
import SearchInputText from "./SearchInputText";
const FACILITY_TYPE_URL_MAP: Record<string, string> = {
tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설
tb_il_swage_prcs_fclt:
"/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&propertyName=swtrmt_fclt_cd,swtrmt_fclt_nm&cql_filter=",
tb_wr_bdst_ruse_fclt:
"/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post
tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get
tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get
tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get
tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get
"/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000/",
tb_wr_rnwtr_utztn_fclt:
"/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}/{fcltyNm}",
tb_wr_rswtr_fclt:
"/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}/{fcltyNm}",
tb_wr_htdfl_fclt:
"/reuse-service/drain/facilityList/1/1000/{sggCd}/{fcltyNm}",
tb_wr_ruse_fclt_dmd_co:
"/reuse-service/reuseFc/request/1/1000/{sggCd}/{fcltyNm}",
tb_wr_tmpr_dmd_co:
"/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get
"/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}/{fcltyNm}",
};
interface SearchBarProps {
@@ -32,80 +35,100 @@ export function SearchBar({
}: 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 [facilityValue, setFacilityValue] = useState("0");
const [facilityType, setFacilityType] = useState(
facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0"
);
const [fcltyNm, setFcltyNm] = useState("");
const [searchList, setSearchList] = useState<
{ bscFctNm: string; bscFctCd: string }[]
>([]);
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 fetchSido = async () => {
const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`;
const res = await fetch(url);
const res = await fetch("/cms-service/api/v1/code/cache/ADSTDSTRCD/Y");
const json = await res.json();
const sidoList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length === 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
setSidoData([{ text: "선택", val: "0" }, ...sidoList]);
.filter((item: any) => item.lwpoCd?.length === 2)
.map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm }));
const sigunList = json
.filter(
(item: any) =>
typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2
)
.map((feature: any) => ({
val: feature.lwpoCd,
text: feature.lwpoCdNm,
}));
.filter((item: any) => item.lwpoCd?.length > 2)
.map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm }));
setSidoData([{ text: "시도", val: "0" }, ...sidoList]);
setSigunData(sigunList);
};
//시설명 콤보박스 조회
const fetchFacilitiesBySggNm = async (
sggCd: string,
sggNm: string,
sidoNm: string,
currentFacilityType?: string
) => {
const type = currentFacilityType ?? facilityType;
if (!type || type === "0") return;
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setFilteredSigunData([{ text: "시도", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]);
setSigunValue("0");
},
[sigunData]
);
useEffect(() => {
fetchSido();
}, []);
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
setSidoValue(val);
initializeSigunData(val);
setSearchList([]);
setFcltyNm("");
} else if (type === "sggCd") {
setSigunValue(val);
setSearchList([]);
setFcltyNm("");
} else if (type === "facilityType") {
setFacilityType(val);
setSearchList([]);
setFcltyNm("");
}
},
[initializeSigunData]
);
const handleSearch = async () => {
if (!fcltyNm) return alert("시설명을 입력하세요.");
const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || "";
const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || "";
const type = facilityType;
const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
if (!baseUrlTemplate) return;
let url = baseUrlTemplate;
const year = new Date().getFullYear().toString();
let url = baseUrlTemplate
url = url
.replace("{year}", year)
.replace("{sggCd}", sggCd)
.replace("{sggCd}", sigunValue)
.replace("{sidoNm}", encodeURIComponent(sidoNm))
.replace("{sggNm}", encodeURIComponent(sggNm));
.replace("{sggNm}", encodeURIComponent(sggNm))
.replace("{fcltyNm}", encodeURIComponent(fcltyNm));
let list: SelectDataType[] = [];
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}`;
if (type === "tb_il_swage_prcs_fclt") {
const cqlFilter = `ctpv_nm='${sidoNm}' and sgg_nm='${sggNm}' and swtrmt_fclt_nm like '%${fcltyNm}%'`;
url += encodeURIComponent(cqlFilter);
const res = await fetch(url);
const json = await res.json();
list = json.features.map((f: any) => ({
@@ -115,18 +138,43 @@ export function SearchBar({
} 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;
//재이용에서 참조하는 하수처리시설 목록
const procRes = await fetch("/reuse-service/reuseFc/pubFc");
const procJson = await procRes.json();
const procData = procJson.data;
const procResult = procData
.filter((code: string) => code.slice(0, 5) === sigunValue)
.map((code: string) => {
return code;
});
//처리시설명으로 처리시설코드 조회
let procUrl2 =
"/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}";
procUrl2 = procUrl2
.replace("{sggCd}", sigunValue)
.replace("{fcltyNm}", encodeURIComponent(fcltyNm));
const procRes2 = await fetch(procUrl2, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(procResult),
});
const procJson2 = await procRes2.json();
const procResult2 = (procJson2.data || []).map(
(item: any) => item.bscFctCd
);
//최종 처리시설코드로 재이용시설조회
const res = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(params),
body: JSON.stringify(procResult2),
});
const json = await res.json();
list = (json.data || []).map((item: any) => ({
@@ -162,78 +210,19 @@ export function SearchBar({
}));
}
setFacilityData([{ text: "선택", val: "0" }, ...list]);
setFacilityValue("0");
};
//시도, 시군구 초기 데이터 조회
useEffect(() => {
fetchSido();
}, []);
//시군구 데이터 초기화
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setFilteredSigunData([{ text: "선택", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]);
setSigunValue("0");
},
[sigunData]
);
//콤보박스 선택시 연관 콤보박스 처리
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
setSidoValue(val);
initializeSigunData(val);
setFacilityData([{ text: "선택", val: "0" }]);
setFacilityValue("0");
} else if (type === "sggCd") {
setSigunValue(val);
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") {
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 || "";
if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
} else if (type === "fcltCd") {
setFacilityValue(val);
}
},
[initializeSigunData, sigunData, sigunValue, sidoData, sidoValue]
);
//조회 버튼 클릭
const handleSearch = () => {
if (!gisMaps.current || facilityValue === "0") return;
const layers = gisMaps.current.getLayers().getArray();
for (const layer of layers) {
const source = layer.getSource?.();
const feature = source?.getFeatureById?.(facilityValue);
if (feature) {
onFeatureSelect(feature);
return;
}
if (list.length === 0) {
alert("검색 결과가 없습니다.");
setSearchList([]);
setFcltyNm("");
return;
}
alert("해당 처리시설을 지도에서 찾을 수 없습니다.");
const newList = list.map((item: any) => {
const code = item.val;
const name = item.text;
return { bscFctNm: name, bscFctCd: code };
});
setSearchList(newList);
};
return (
@@ -244,10 +233,13 @@ export function SearchBar({
{facilityTypes.length > 1 && (
<SelectBox
labelClass="w-[120px]"
className="mr-10 w-[190px]"
className="mr-5 w-[150px]"
selectClass="rounded-lg bg-white"
label="시설물유형"
selectArray={[{ text: "선택", val: "0" }, ...facilityTypes]}
label=""
selectArray={[
{ text: "시설 구분", val: "0" },
...facilityTypes,
]}
onSelectValue={(val) =>
onSelectValue(val.toString(), "facilityType")
}
@@ -256,37 +248,49 @@ export function SearchBar({
)}
<SelectBox
labelClass="w-[40px]"
className="mr-10 w-[150px]"
className="mr-5 w-[150px]"
selectClass="rounded-lg bg-white"
label="시도"
label=""
selectArray={sidoData}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="mr-10 w-[150px]"
className="mr-5 w-[150px]"
selectClass="rounded-lg bg-white"
label="시군구"
label=""
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<SelectBox
labelClass="w-[60px]"
className="mr-3 w-[180px]"
selectClass="rounded-lg bg-white"
<SearchInputText
label="시설명"
selectArray={facilityData}
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
value={facilityValue}
initValue={fcltyNm}
setValue={(val: string = "") => {
setFcltyNm(val);
}}
setRtnData={(selected: {
bscFctNm: string;
bscFctCd: string;
}) => {
// 선택된 항목이 있을 경우 onFeatureSelect 실행
const type = facilityType;
onFeatureSelect({
layerNm: type,
...selected,
});
}}
searchList={searchList}
onEnter={handleSearch}
className="mr-5 w-[150px]"
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="primary"
className="mx-2 mt-1 ml-5"
size={"sm"}
className="mx-2 mt-1"
size="sm"
onClick={handleSearch}
>
조회
+11
View File
@@ -9,6 +9,7 @@ const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const REUSE_API_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL || "";
const OPERATION_API_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL || "";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
@@ -145,6 +146,15 @@ export function middleware(req: NextRequest) {
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/operation-service")) {
const queryParams = searchParams.toString();
const newUrl = `${OPERATION_API_URL}${pathname.replace("/operation-service", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
return NextResponse.next();
}
@@ -157,6 +167,7 @@ export const config = {
"/geoserver/:path*",
"/sewerage-service/:path*",
"/reuse-service/:path*",
"/operation-service/:path*",
"/"
],
};