10. 오염률 표시
오염률 예측 공식

프로젝트 멘토님께서 제시해 주신 이 공식을 바탕으로 지하수 흐름과 융합하여 최종 오염률을 예측하는 기능을 구현했습니다.
구현 코드
useState로 값을 받고 공식 적용해서 ΔC
을 구합니다.
import React, { useState } from "react";
import image2 from "./svg/image-2.svg";
import image from "./svg/image.svg";
import "./InfoPanel.css";
export const InfoPanel = ({ setDeltaC }) => {
const [isVisible, setIsVisible] = useState(true);
const [temperature, setTemperature] = useState("");
const [rainfall, setRainfall] = useState("");
const [humidity, setHumidity] = useState("");
const [windspeed, setWindspeed] = useState("");
const [discharge, setDischarge] = useState(""); // 유출량(Q)
const togglePanel = () => {
setIsVisible(!isVisible);
};
const handleSubmit = () => {
if (!temperature.trim()) {
alert("기온을 입력해주세요.");
document.getElementById("temperature-input").focus();
return;
}
if (!rainfall.trim()) {
alert("강수량을 입력해주세요.");
document.getElementById("rainfall-input").focus();
return;
}
if (!humidity.trim()) {
alert("습도를 입력해주세요.");
document.getElementById("humidity-input").focus();
return;
}
if (!windspeed.trim()) {
alert("풍속을 입력해주세요.");
document.getElementById("windspeed-input").focus();
return;
}
if (!discharge.trim()) {
alert("유출량을 입력해주세요.");
document.getElementById("discharge-input").focus();
return;
}
if (isNaN(Number(temperature))) {
alert("기온을 숫자로 입력하세요.");
setTemperature("");
document.getElementById("temperature-input").focus();
return;
}
if (isNaN(Number(rainfall))) {
alert("강수량을 숫자로 입력하세요.");
setRainfall("");
기
const ΔC = 0.12 * Number(rainfall)
+ 0.06 * Number(temperature)
- 0.015 * Number(humidity)
+ 0.05 * Number(windspeed)
+ Number(discharge);
console.log("📌 계산된 ΔC:", ΔC); // ✅ 이 줄 추가
setDeltaC(ΔC); // 💡 App.js의 상태 업데이트
};
return (
<div className="box">
<div className="panel-wrapper">
{isVisible && (
<div className="div weather-form">
<div className="text-wrapper">날씨 정보 입력</div>
<img className="image" alt="Image" src={image2} />
<div className="view-2">
<div className="overlap-group">
<div className="element">
<input id="temperature-input" type="text" className="view-3" value={temperature} onChange={(e) => setTemperature(e.target.value)} />
<div className="text-wrapper-2">기온</div>
</div>
<div className="text-wrapper-3">℃</div>
</div>
<div className="element-2">
<input id="rainfall-input" type="text" className="view-3" value={rainfall} onChange={(e) => setRainfall(e.target.value)} />
<div className="text-wrapper-2">강수량</div>
<div className="text-wrapper-5">mm</div>
</div>
<div className="element-3">
<input id="humidity-input" type="text" className="view-3" value={humidity} onChange={(e) => setHumidity(e.target.value)} />
<div className="text-wrapper-2">습도</div>
<div className="text-wrapper-6">%</div>
</div>
<div className="overlap">
<div className="element">
<input id="windspeed-input" type="text" className="view-3" value={windspeed} onChange={(e) => setWindspeed(e.target.value)} />
<div className="text-wrapper-2">풍속</div>
</div>
<div className="text-wrapper-4">m/s</div>
</div>
<div className="element-4">
<input id="discharge-input" type="text" className="view-3" value={discharge} onChange={(e) => setDischarge(e.target.value)} />
<div className="text-wrapper-2">유출량</div>
</div>
<div className="text-wrapper-7">kg</div>
{/* <div className="text-wrapper-5">mm</div>
<div className="text-wrapper-6">%</div> */}
<button className="submit-button" onClick={handleSubmit}>입력하기</button>
</div>
</div>
)}
<div className="image-wrapper" style=>
<button onClick={togglePanel}>
<img className={`img ${isVisible ? "open" : "closed"}`} alt="Toggle" src={image} />
</button>
</div>
</div>
</div>
);
};
계산된 ΔC
을 NaverMapComponent.js
에 넘기기 위해서 모든 컴포넌트의 부모인 App.js
로 넘깁니다.
import React, { useState } from "react";
import Joyride from "react-joyride";
import NaverMapComponent from "./components/NaverMapComponent";
import { TopPanel } from "./components/TopPanel";
import { InfoPanel } from "./components/InfoPanel";
const App = () => {
const [run, setRun] = useState(true);
const [showDEM, setShowDEM] = useState(false);
const [showWatershed, setShowWatershed] = useState(false);
const [showPollution, setShowPollution] = useState(false);
const [showTerrain, setShowTerrain] = useState(false);
const [deltaC, setDeltaC] = useState(null); // ΔC 상태
const steps = [
// {
// target: ".top-toggle-buttons",
// content: "상단 기능 버튼들입니다. 수질 예측 또는 주제도 기능을 선택하세요.",
// },
{
target: ".top-toggle-buttons", // ✅ 상단 전체 버튼 영역
content: "DEM과 오염원, 하천 유역, 위성 지도 기능을 선택할 수 있어요.",
placement: "bottom",
spotlightPadding: 20, // 옵션: 영역을 더 넓게 강조하고 싶을 때
},
{
target: ".weather-form",
content: "여기에 날씨 데이터를 입력해주세요.",
},
{
target: "#map",
content: "지도를 확대하거나 마커를 클릭해 정보를 확인할 수 있습니다.",
},
];
return (
<div style=>
<Joyride
steps={steps}
run={run}
showSkipButton
showProgress
continuous
styles={{
options: {
primaryColor: "#007bff", // ⬅️ 여기서 Next/Done 버튼 색 변경
backgroundColor: "#ffffff",
textColor: "#333333",
arrowColor: "#ffffff",
spotlightPadding: 20,
zIndex: 10000,
},
}}
/>
<NaverMapComponent
showDEM={showDEM}
showWatershed={showWatershed}
showPollution={showPollution}
showTerrain={showTerrain}
deltaC={deltaC}
/>
<div style=>
<TopPanel
onToggleDEM={setShowDEM}
onToggleWatershed={setShowWatershed}
onTogglePollution={setShowPollution}
onToggleTerrain={setShowTerrain}
showDEM={showDEM}
showWatershed={showWatershed}
showPollution={showPollution}
showTerrain={showTerrain}
/>
</div>
<div style=>
<InfoPanel setDeltaC={setDeltaC}/>
</div>
</div>
);
};
export default App;
메인화면인 NaverMapComponent.js
에서 최종적으로 ΔC
를 이용해서 오염률을 표시해줍니다.
import React, { useEffect, useRef, useState } from "react";
import * as turf from "@turf/turf";
const NaverMapComponent = ({ showDEM, showWatershed, showPollution, showTerrain, deltaC }) => {
const [geoJsonData, setGeoJsonData] = useState(null);
const [watershedPolygons, setWatershedPolygons] = useState([]);
const [pollutionMarkers, setPollutionMarkers] = useState([]);
const [demOverlay, setDemOverlay] = useState(null);
const currentLineRef = useRef(null);
const mapRef = useRef(null);
const infoWindowRef = useRef(null);
// 🔽 컴포넌트 최상단에 추가
const watershedCirclesRef = useRef([]);
const MAX_DELTA_C = 25;
const getPollutionRatePercent = (deltaC) => {
return Math.min((deltaC / MAX_DELTA_C) * 100, 100);
};
const getPolylineColorByPollution = (percent) => {
if (percent >= 91) return "#FF0000";
if (percent >= 71) return "#FF8000";
if (percent >= 51) return "#00AA00";
if (percent >= 30) return "#00CFFF";
return "#AAAAAA";
};
useEffect(() => {
console.log("\ud83d\udce1 NaverMapComponent\uc5d0\uc11c \ubc1b\uc740 deltaC:", deltaC);
}, [deltaC]);
useEffect(() => {
const map = new window.naver.maps.Map("map", {
center: new window.naver.maps.LatLng(37.926, 127.75),
zoom: 13,
mapTypeId: window.naver.maps.MapTypeId.NORMAL,
});
mapRef.current = map;
}, []);
useEffect(() => {
if (!mapRef.current) return;
mapRef.current.setMapTypeId(
showTerrain ? window.naver.maps.MapTypeId.HYBRID : window.naver.maps.MapTypeId.NORMAL
);
}, [showTerrain]);
useEffect(() => {
const drawWatershed = async () => {
if (!mapRef.current) return;
if (watershedPolygons.length > 0) {
watershedPolygons.forEach((poly) => {
poly.setOptions({
fillOpacity: showWatershed ? 0.3 : 0.0,
strokeOpacity: showWatershed ? 0.8 : 0.0,
});
});
return;
}
try {
const res = await fetch("/data/clip.geojson");
const geojson = await res.json();
setGeoJsonData(geojson);
const newPolygons = geojson.features.map((feature) => {
const coords =
feature.geometry.type === "Polygon"
? feature.geometry.coordinates[0]
: feature.geometry.coordinates[0][0];
const path = coords.map(
([lng, lat]) => new window.naver.maps.LatLng(lat, lng)
);
return new window.naver.maps.Polygon({
map: mapRef.current,
paths: path,
strokeColor: "#8000FF",
strokeOpacity: showWatershed ? 0.8 : 0.0,
strokeWeight: 2,
fillColor: "#A56AFF",
fillOpacity: showWatershed ? 0.3 : 0.0,
});
});
setWatershedPolygons(newPolygons);
} catch (err) {
console.error("\u274c clip.geojson \ub85c\ub4dc \uc2e4\ud328:", err);
}
};
drawWatershed();
}, [showWatershed]);
useEffect(() => {
const loadMarkers = async () => {
if (!mapRef.current) return;
if (!showPollution) {
pollutionMarkers.forEach((marker) => marker.setMap(null));
setPollutionMarkers([]);
return;
}
try {
const res = await fetch("http://localhost:8080/pollution-sources");
const data = await res.json();
const newMarkers = [];
for (const place of data) {
const markerPosition = new window.naver.maps.LatLng(
place.web_bplc_x_katec,
place.web_bplc_y_katec
);
const marker = new window.naver.maps.Marker({
position: markerPosition,
map: mapRef.current,
title: place.bsnm_nm,
});
window.naver.maps.Event.addListener(marker, "click", async () => {
// 📌 ΔC 미입력 시 알림 후 조기 반환
if (deltaC === null) {
alert("변인들을 먼저 입력해주십시오.");
return;
}
// ✅ 항상 실행되는 초기화 코드
if (currentLineRef.current) {
currentLineRef.current.setMap(null);
currentLineRef.current = null;
}
if (watershedCirclesRef.current) {
watershedCirclesRef.current.forEach(c => c.setMap(null));
watershedCirclesRef.current = [];
}
if (infoWindowRef.current) {
infoWindowRef.current.close();
}
try {
const res = await fetch(`/data/flow/flow_path_${place.id}.geojson`);
const flowGeojson = await res.json();
const coords = flowGeojson.features[0].geometry.coordinates.map(
([lng, lat]) => new window.naver.maps.LatLng(lat, lng)
);
let intersects = false;
let strokeColor = "#AAAAAA";
let pollutionPercent = 0;
if (geoJsonData) {
const polygon = turf.featureCollection(geoJsonData.features);
const line = turf.lineString(flowGeojson.features[0].geometry.coordinates);
intersects = turf.booleanIntersects(polygon, line);
if (intersects && deltaC !== null) {
pollutionPercent = getPollutionRatePercent(deltaC);
strokeColor = getPolylineColorByPollution(pollutionPercent);
}
// 🔽 마커 클릭 이벤트 내부 try 블록 안 intersect 검사 이후에 추가
if (intersects && deltaC !== null) {
pollutionPercent = getPollutionRatePercent(deltaC);
strokeColor = getPolylineColorByPollution(pollutionPercent);
let invalidGeometryCount = 0;
geoJsonData.features.forEach((feature) => {
const geometry = feature.geometry;
if (!geometry || geometry.coordinates.length === 0) return;
let polygons = [];
if (geometry.type === "Polygon") {
polygons = [geometry.coordinates];
} else if (geometry.type === "MultiPolygon") {
polygons = geometry.coordinates;
} else {
return; // Unknown geometry type → skip
}
polygons.forEach((polyCoords) => {
const poly = turf.polygon(polyCoords);
const intersections = turf.lineIntersect(poly, line);
// ✅ 모든 교차점에 원을 그림
intersections.features.forEach((pt) => {
const [lng, lat] = pt.geometry.coordinates;
const centerLatLng = new window.naver.maps.LatLng(lat, lng);
const circle = new window.naver.maps.Circle({
map: mapRef.current,
center: centerLatLng,
radius: 300,
strokeColor: strokeColor,
strokeOpacity: 1,
strokeWeight: 1.5,
fillColor: strokeColor,
fillOpacity: 0.3,
});
watershedCirclesRef.current.push(circle);
});
});
});
// ✅ 최종 통계 출력
if (invalidGeometryCount > 0) {
console.warn(`⚠️ 총 ${invalidGeometryCount}개의 유효하지 않은 geometry가 존재합니다.`);
}
}
}
const polyline = new window.naver.maps.Polyline({
map: mapRef.current,
path: coords,
strokeColor: strokeColor,
strokeOpacity: 0.9,
strokeWeight: 3,
});
currentLineRef.current = polyline;
const infoHtml = `
<div style="
background:white;
border:1px solid #888;
border-radius:8px;
padding:10px;
font-size:13px;
line-height:1.5;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
white-space:nowrap;
">
<strong>${place.bsnm_nm}</strong><br/>
${place.bsns_detail_road_addr}<br/>
${place.induty_nm}<br/>
${intersects ? `오염률: ${pollutionPercent.toFixed(1)}%` : "<span style='color:gray'>하천 유역과 맞닿지 않음</span>"}
</div>`;
const infoWindow = new window.naver.maps.InfoWindow({
content: infoHtml,
pixelOffset: new window.naver.maps.Point(0, -60),
disableAutoPan: true,
});
infoWindow.open(mapRef.current, marker);
infoWindowRef.current = infoWindow;
mapRef.current.panTo(marker.getPosition());
} catch (err) {
alert("흐름 경로를 불러오지 못했습니다.");
console.error(err);
}
});
newMarkers.push(marker);
}
setPollutionMarkers(newMarkers);
} catch (err) {
console.error("\u274c 오염원 API 호출 에러:", err);
}
};
loadMarkers();
}, [showPollution, geoJsonData, deltaC]);
useEffect(() => {
if (!mapRef.current) return;
if (showDEM) {
const bounds = new window.naver.maps.LatLngBounds(
new window.naver.maps.LatLng(37.7331240, 127.4980451),
new window.naver.maps.LatLng(38.0200258, 127.9446376)
);
const overlay = new window.naver.maps.GroundOverlay(
"/images/hillshade.png",
bounds,
{
map: mapRef.current,
opacity: 0.6,
}
);
setDemOverlay(overlay);
} else {
if (demOverlay) {
demOverlay.setMap(null);
setDemOverlay(null);
}
}
}, [showDEM]);
return (
<div style=>
<div id="map" style= />
</div>
);
};
export default NaverMapComponent;
실제 화면

느낀점
이번 프로젝트를 통해서 딥러닝과 데이터 분석을 구현해볼 수 있는 기회가 생겨서 좋았습니다. 학교 정규 강의로는 잘 다루지 않는 부분들이어서 언젠가 한번 도전해보고 싶은 생각이 있었는데 이번 기회에 해볼 수 있어서 다음에도 기회가 생기면 할 수 있겠다는 자신감이 생겼습니다.
그리고 도메인 영역의 중요성에 대해서 깨달았습니다. 아무리 개발능력과 구현능력이 뛰어나다고 해도 내가 코딩하려는 그 전문 분야에 대한 배경지식이 없으면 앞길이 막막합니다. 그래서 개발능력을 키우는 것도 중요하지만 코딩해야할 분야의 지식을 빠르고 정확하게 습득하는 능력도 중요하다고 느꼈습니다.