https://this-circle-jeong.tistory.com/244

지도 : 카카오 API

날씨 : 기상청 공공 포탈 데이터

스크린샷 2025-04-24 112146.png

스크린샷 2025-04-24 112225.png

장소를 검색하면 마커로 위치가 나오고 옆의 그 날씨를 표시한다

index.jsp

<%@ page contentType="text/html; charset=UTF-8" language="java"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Index</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
	<div id="wrapper">
		<header>
			<img src="img/weather.png" alt="날씨 아이콘">
			<p>날씨 단기예보 검색</p>
		</header>
		<main>
			<div class="ct_msg">
				<p>장소를 검색하세요</p>
			</div>
			<form action="search.jsp" method="GET"> //serch페이지 겟방식으로 이동
				<div class="search_bar">
					<img src="img/search.png" alt="검색 아이콘"> 
					<input class="searchText" name="location" type="text"
						placeholder="장소를 입력하세요">
						<button class="btnSearch" type="submit">검색</button>
				</div>
			</form>
		</main>	
		<footer>
			<div class="ct_footer">Copyright© jjh0807a All RIGHTS RESERVED.</div>
		</footer>
	</div>
</body>
</html>

search.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%
String location = request.getParameter("location");//사용자가 입력한 지역명 가져오기
%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>검색</title>
<link rel="stylesheet" href="css/style.css">

<!-- 카카오 지도 API -->
<script type="text/javascript"
	src="<https://dapi.kakao.com/v2/maps/sdk.js?appkey=**발급받은키**services>"></script>

<script>
window.onload = function () { //메인 시작, 페이지 로딩이 완료되면 실행
    var container = document.querySelector('.ct_map'); // 지도 담을 컨테이너
    var options = { //지도초기설정
        center: new kakao.maps.LatLng(37.5665, 126.9780), // 서울시청 (기본 위치)
        level: 3 //확대할 사이즈
    };

    var map = new kakao.maps.Map(container, options); // 지도 생성

    //url에서 location 파라미터 추출
    var locationParam = new URLSearchParams(window.location.search).get('location'); // url에 location이라는 이름의 검색어 포함

    // 검색어가 입력되었는지 확인
    if (locationParam) { //만약 사용자가 검색한 location이 값이 있을 때,
        var geocoder = new kakao.maps.services.Geocoder(); // 주소-좌표 변환 객체 생성

        //입력된 주소명으로 좌표 검색
        geocoder.addressSearch(locationParam, function(result, status) {
            if (status === kakao.maps.services.Status.OK) { // 정상적으로 검색되었을 경우
                console.log("정상적으로 검색");

                var coords = new kakao.maps.LatLng(result[0].y, result[0].x); // 검색된 위도, 경도 추출
                var lat = coords.getLat();//위도
                var lon = coords.getLng();//경도

                var marker = new kakao.maps.Marker({ // 마커 생성(location표시)//let으로도 가능
                    map: map,  //자바스크립트의 객체리터럴 : 코드 상에서 직접 객체를 정의할 때 쓰는 문법
                    position: coords
                });

                //인포윈도우 생성
                var infowindow = new kakao.maps.InfoWindow({ // 인포윈도우 표시
                    content: '<div style="width:150px;text-align:center;padding:6px 0;">' + locationParam + '</div>'
                });
                infowindow.open(map, marker);

                map.setCenter(coords); // 지도 중심 이동
                console.log(coords);//la,ma (coords=LatLng 객체하나)
                console.log(typeof coords);//object
                fetchWeather(lat, lon, locationParam);  // 날씨 정보 가져오기
            } else {
                console.log("정상적인 검색이 아님, 기본 위치(서울시청)로 이동");

                // 기본 위치 (서울시청) 좌표로 이동
                var defaultCoords = new kakao.maps.LatLng(37.5665, 126.9780); // 서울시청

                // 마커생성 : 장소가 나오지 않을 때
                var marker = new kakao.maps.Marker({
                    map: map,
                    position: defaultCoords
                });
                             map.setCenter(defaultCoords); // 지도 중심 서울시청으로
                fetchWeather(37.5665, 126.9780, "서울");  // 기본 위치 서울의 날씨 정보
            }
        }); // 지오코더 끝
    } // if (locationParam) 끝
}; // 메인 끝

function getWindDirection(degree) { //풍향>>문자 (degree=각도, 0~360도)
    const dirs = ["북", "북동", "동", "남동", "남", "남서", "서", "북서"];
    const ix = Math.round(degree / 45) % 8; //360도원을 45도씩 나눈 8개 구간,%8은 인덱스가 8이상 되는 걸 방지
    return dirs[ix] + "풍"; //나온 문자열에 "풍"을 붙여서 return;
}

// 날씨 정보 가져오는 함수
function fetchWeather(lat, lon, location) {
    fetch("weatherapi.jsp?lat=" + lat + "&lon=" + lon + "&location=" + encodeURIComponent(location))
        .then(res => res.json()) //화살표함수, function 키워드를 쓰지 않고 (매개변수)=>{함수내용} 형태로 간단하게 함수를 정의하는 문법
        .then(data => {
            const skyMap = { "1": "☀️ 맑음", "3": "⛅ 구름많이", "4": "☁️ 흐림" };
            const ptyMap = { "0": "🌤️ 구름조금", "1": "🌧️ 비", "2": "🌨️ 비/눈", "3": "❄️ 눈", "4": "🌦️ 소나기" };
//fetch("어디에 요청해")
// .then(응답이 오면 그걸 JSON으로 바꿔)
// .then(JSON이 오면 그걸로 화면에 날씨 출력해)
            
              // 문자열을 + 로 연결해서 출력 ★★★
          const html = 
  '<div class="weather-wrapper">' +
    '<div class="weather-header">' +
      '<div class="weather-temp">' + data.temp + '°</div>' +
      '<div class="weather-condition">' + (skyMap[data.sky] || "") + ' ' + (ptyMap[data.pty] || "") + '</div>' +
    '</div>' +
    '<div class="weather-cards">' +
      '<div class="weather-card rain">' +
        '<div class="label">강수</div>' +
        '<div class="value">' + (data.pty === "0" ? "없음" : ptyMap[data.pty]) + '</div>' +
      '</div>' +
      '<div class="weather-card humidity">' +
        '<div class="label">습도</div>' +
        '<div class="value">' + data.reh + '%</div>' +
      '</div>' +
      '<div class="weather-card wind">' +
        '<div class="label">바람</div>' +
        '<div class="value">' + getWindDirection(data.vec) + ' ' + data.wsd + 'm/s</div>' +
      '</div>' +
   
  '</div>';

            // .ct_weather에 HTML 삽입
            document.querySelector(".ct_weather").innerHTML = html;
        })
        .catch(err => {
            console.error("날씨 API 오류", err);
            document.querySelector(".ct_weather").innerHTML = "날씨 정보를 불러오는 데 실패했습니다.";
        });
}
</script>

</head>
<body>
	<div id="wrapper">
		<header>
			<img src="img/weather.png" alt="날씨 아이콘">
			<p>날씨 단기예보 검색</p>
		</header>

		<main>
			<div class="ct_search">
				<form action="search.jsp" method="get">
					<div class="search_bar">
						<img src="img/search.png" alt="검색 아이콘"> <input
							class="searchText" type="text" name="location"
							placeholder="행정구역(시.도/시.군.구/읍.면.동)으로 검색">
						<button type="submit" class="btnSearch">검색</button>
					</div>
				</form>
			</div>

			<div class="ly_content">
				<div class="ct_map">지도</div>
				<div class="ct_weather">날씨</div>
			</div>
		</main>

		<footer>
			<div class="ct_footer">Copyright© jjh0807a Rights RESERVED.</div>
		</footer>
	</div>
</body>
</html>

whetherapi.jsp

<%@ page contentType="application/json; charset=UTF-8" %>
<%@ page import="java.net.*, java.io.*, org.json.simple.*, org.json.simple.parser.*,java.text.SimpleDateFormat" %>
<%!

// 위경도 → 격자 변환용 클래스 & 함수
class LatXLngY {
    public double lat;
    public double lng;
    public int x;
    public int y;
}

public LatXLngY convertGRID(double lat, double lng) {
    double RE = 6371.00877; // 지구 반경(km)
    double GRID = 5.0; // 격자 간격(km)
    double SLAT1 = 30.0; // 투영 위도1(degree)
    double SLAT2 = 60.0; // 투영 위도2(degree)
    double OLON = 126.0; // 기준점 경도
    double OLAT = 38.0; // 기준점 위도
    double XO = 43; // 기준점 X좌표(GRID)
    double YO = 136; // 기준점 Y좌표(GRID)

    double DEGRAD = Math.PI / 180.0;
    double re = RE / GRID;
    double slat1 = SLAT1 * DEGRAD;
    double slat2 = SLAT2 * DEGRAD;
    double olon = OLON * DEGRAD;
    double olat = OLAT * DEGRAD;

    double sn = Math.tan(Math.PI * 0.25 + slat2 * 0.5) / Math.tan(Math.PI * 0.25 + slat1 * 0.5);
    sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(sn);
    double sf = Math.tan(Math.PI * 0.25 + slat1 * 0.5);
    sf = Math.pow(sf, sn) * Math.cos(slat1) / sn;
    double ro = Math.tan(Math.PI * 0.25 + olat * 0.5);
    ro = re * sf / Math.pow(ro, sn);

    LatXLngY rs = new LatXLngY();
    rs.lat = lat;
    rs.lng = lng;

    double ra = Math.tan(Math.PI * 0.25 + (lat) * DEGRAD * 0.5);
    ra = re * sf / Math.pow(ra, sn);
    double theta = lng * DEGRAD - olon;
    if (theta > Math.PI) theta -= 2.0 * Math.PI;
    if (theta < -Math.PI) theta += 2.0 * Math.PI;
    theta *= sn;

    rs.x = (int)(Math.floor(ra * Math.sin(theta) + XO + 0.5));
    rs.y = (int)(Math.floor(ro - ra * Math.cos(theta) + YO + 0.5));

    return rs;
}
%>
<%
try { //search.jsp에서 fetch로 전달한 위도/경도
    String lat = request.getParameter("lat");//latitude
    String lon = request.getParameter("lon");
    String location = request.getParameter("location");

    // 1. 위도/경도를 격자(x, y)로 변환 (예시 고정값 또는 추후 계산)
     double latVal = Double.parseDouble(lat);
    double lonVal = Double.parseDouble(lon);
    LatXLngY grid = convertGRID(latVal, lonVal);
    int nx = grid.x;
    int ny = grid.y;

    // 2. 날짜/시간 계산
    java.util.Calendar cal = java.util.Calendar.getInstance();
    int minute = cal.get(java.util.Calendar.MINUTE);
    if (minute < 30) {
        cal.add(java.util.Calendar.HOUR_OF_DAY, -1);
        minute += 30;
    } else {
        minute -= 30;
    }
    java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyyMMdd");
    String base_date = dateFormat.format(cal.getTime());
    String base_time = String.format("%02d%02d", cal.get(java.util.Calendar.HOUR_OF_DAY), minute);

    // 3. 기상청 API 호출
    String serviceKey = "**발급받은키**";
    String apiUrl = "<https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst>"
        + "?serviceKey=" + serviceKey
        + "&pageNo=1&numOfRows=1000&dataType=JSON"
        + "&base_date=" + base_date
        + "&base_time=" + base_time
        + "&nx=" + nx + "&ny=" + ny;

    URL url = new URL(apiUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("GET");

    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = br.readLine()) != null) sb.append(line);
    br.close();

    // 4. JSON 파싱
    JSONParser parser = new JSONParser();
    JSONObject responseObj = (JSONObject) parser.parse(sb.toString());
    JSONObject body = (JSONObject) ((JSONObject) responseObj.get("response")).get("body");
    JSONObject items = (JSONObject) body.get("items");
    JSONArray itemArr = (JSONArray) items.get("item");

    String temp = "", sky = "", reh = "", wsd = "", pty = "", vec = "";
    for (Object obj : itemArr) {
        JSONObject item = (JSONObject) obj;
        String category = (String) item.get("category");
        String value = (String) item.get("fcstValue");
      
        if (category.equals("T1H")) {//기온
            temp = value;
        } else if (category.equals("SKY")) {//하늘상태
            sky = value;
        } else if (category.equals("REH")) {//강수형태
            reh = value;
        } else if (category.equals("WSD")) {//습도
            wsd = value;
        } else if (category.equals("PTY")) {//풍속
            pty = value;
        } else if (category.equals("VEC")) {//풍향
            vec = value;
        }
      
    }

     //json파싱 
     //기상청에서 준 데이터는 문자열
    JSONObject result = new JSONObject();
    result.put("temp", temp);
    System.out.println("temp= "+temp);
    result.put("sky", sky); 
    System.out.println("sky= "+sky);
    result.put("reh", reh); 
    result.put("wsd", wsd);
    result.put("pty", pty);
    result.put("vec", vec);
    out.print(result.toJSONString());

} catch (Exception e) {
    e.printStackTrace();
    JSONObject error = new JSONObject();
    error.put("error", "데이터 처리 중 오류 발생");
    out.print(error.toJSONString());
}
%>