๐Ÿ“Œ Leaflet + Daum ์ง€๋„ + OpenWeatherMap ์—ฐ๋™ ํ”„๋กœ์ ํŠธ

My Pale Blue Dotยท2025๋…„ 5์›” 12์ผ

SPRING BOOT

๋ชฉ๋ก ๋ณด๊ธฐ
15/40
post-thumbnail

๐Ÿ“… 2025-05-12


๐Ÿงญ ๊ฐœ์š” ๋ฐ ํ•™์Šต ๋ชฉ์ 

๐Ÿ” ๋ฌด์—‡์„ ๊ตฌํ˜„ํ–ˆ๋‚˜?

  • ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ Daum ๊ธฐ๋ฐ˜ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ง€๋„๋ฅผ ๋„์šฐ๊ณ ,
  • ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญํ•œ ์œ„์น˜์˜ ์‹ค์‹œ๊ฐ„ ๋‚ ์”จ ์ •๋ณด๋ฅผ OpenWeatherMap API๋กœ ๋ฐ›์•„,
  • ์ง€๋„ ์œ„์— ๋งˆ์ปค์™€ ํŒ์—… ํ˜•ํƒœ๋กœ ์‹œ๊ฐํ™”ํ•˜๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ๊ตฌํ˜„ํ•จ.

๐Ÿง  ์‚ฌ์šฉํ•œ ๊ธฐ์ˆ  ์Šคํƒ

๋ถ„๋ฅ˜์‚ฌ์šฉ ๊ธฐ์ˆ 
์ง€๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌLeaflet.js
์ขŒํ‘œ๊ณ„ ๋ณ€ํ™˜proj4.js + proj4leaflet.js
ํ•œ๊ตญํ˜• ํƒ€์ผ ์†Œ์ŠคLeaflet.KoreanTmsProviders.js
๋ฐฑ์—”๋“œ ํ”„๋ ˆ์ž„์›ŒํฌSpring Boot (Java)
HTTP ํ†ต์‹ Axios (ํ”„๋ก ํŠธ), RestTemplate (๋ฐฑ์—”๋“œ)
์™ธ๋ถ€ APIOpenWeatherMap (๋‚ ์”จ ์ •๋ณด)

๐Ÿ“‚ ์ „์ฒด ํ๋ฆ„ ์š”์•ฝ

1. ์‚ฌ์šฉ์ž๊ฐ€ ์ง€๋„ ํด๋ฆญ
2. ํด๋ฆญํ•œ ์œ„๋„/๊ฒฝ๋„๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†ก
3. ์„œ๋ฒ„๋Š” OpenWeatherMap API์— ๋‚ ์”จ ์ •๋ณด ์š”์ฒญ
4. ๋ฐ›์€ JSON ์‘๋‹ต์„ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ถ”์ถœํ•ด ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ˜ํ™˜
5. ํ”„๋ก ํŠธ๋Š” ํŒ์—… + ๋งˆ์ปค๋กœ ๋‚ ์”จ ์‹œ๊ฐํ™”

๐Ÿ—บ๏ธ index.html ๊ตฌ์„ฑ

1๏ธโƒฃ Head โ€“ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/js/KoreanTmsProviders/lib/proj4.js"></script>
<script src="/js/KoreanTmsProviders/lib/proj4leaflet.js"></script>
<script src="/js/KoreanTmsProviders/src/Leaflet.KoreanTmsProviders.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.9.0/axios.min.js"></script>

2๏ธโƒฃ Body โ€“ ์ง€๋„ ๋ฐ ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€

<div id="loading" style="display:none;position:absolute;top:20px;left:20px;background:#fff;padding:5px;z-index:9999;">
  ๐ŸŒค๏ธ ๋‚ ์”จ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...
</div>
<div id="map" style="height:100vh;width:100%;"></div>

3๏ธโƒฃ Script โ€“ ์ง€๋„ ์ƒ์„ฑ ๋ฐ ๋‚ ์”จ ํŒ์—… ์ฒ˜๋ฆฌ

let currentMarker = null; // ๊ธฐ์กด ๋งˆ์ปค ์ œ๊ฑฐ์šฉ

const map = new L.Map('map', {
  center: new L.LatLng(35.829890, 128.532719),
  zoom: 8,
  crs: L.Proj.CRS.Daum // DaumMap ์ขŒํ‘œ๊ณ„ (EPSG:5181)
});
L.tileLayer.koreaProvider('DaumMap.Street').addTo(map);

const centerMarker = L.marker([35.829890, 128.532719]).addTo(map);
centerMarker.bindTooltip("๋Œ€๊ตฌ").openTooltip();
centerMarker.bindPopup("<div>์ค‘์‹ฌ ์ง€์ </div>");

// ์ง€๋„ ํด๋ฆญ ์‹œ ๋™์ž‘
map.on('click', function(e) {
  const lat = e.latlng.lat;
  const lon = e.latlng.lng;

  if (currentMarker) map.removeLayer(currentMarker); // ์ด์ „ ๋งˆ์ปค ์ œ๊ฑฐ
  currentMarker = L.marker([lat, lon]).addTo(map);
  document.getElementById("loading").style.display = "block";

  axios.get(`/open/weather/get/${lat}/${lon}`)
    .then(resp => {
      const data = resp.data;

      // OpenWeatherMap API๋Š” Kelvin ๋‹จ์œ„๋ฅผ ์‚ฌ์šฉ โ†’ ์„ญ์”จ๋กœ ๋ณ€ํ™˜
      const temp = Math.round(data.temp - 273.15);
      const feels = Math.round(data.feelsLike - 273.15);

      const content = `
        <div style="text-align:center;">
          <img src="https://openweathermap.org/img/wn/${data.icon}@2x.png" /><br/>
          <strong>${data.name}</strong><br/>
          ${data.description} / ${temp}ยฐC<br/>
          ์ฒด๊ฐ์˜จ๋„: ${feels}ยฐC<br/>
          ์Šต๋„: ${data.humidity}% | ๋ฐ”๋žŒ: ${data.windSpeed} m/s
        </div>`;
      currentMarker.bindPopup(content).openPopup();
    })
    .catch(err => {
      alert("๋‚ ์”จ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.");
      console.error(err);
    })
    .finally(() => {
      document.getElementById("loading").style.display = "none";
    });
});

โ˜๏ธ OpenWeatherController.java

@RestController
@RequestMapping("/open/weather")
public class OpenWeatherController {

    // โš ๏ธ ๋ฐ˜๋“œ์‹œ ๋ณธ์ธ์˜ OpenWeatherMap API ํ‚ค๋กœ ๊ต์ฒดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
    private final String serviceKey = "YOUR_API_KEY_HERE";

    @GetMapping("/get/{lat}/{lon}")
    public ResponseEntity<WeatherDto> get(@PathVariable String lat, @PathVariable String lon) throws UnsupportedEncodingException {
        URI uri = UriComponentsBuilder.fromHttpUrl("https://api.openweathermap.org/data/2.5/weather")
            .queryParam("appid", URLEncoder.encode(serviceKey, "UTF-8"))
            .queryParam("lat", lat)
            .queryParam("lon", lon)
            .build(true).toUri();

        RestTemplate rt = new RestTemplate();
        ResponseEntity<Map> response = rt.exchange(uri, HttpMethod.GET, null, Map.class);
        Map body = response.getBody();

        Map main = (Map) body.get("main");
        List<Map> weatherList = (List<Map>) body.get("weather");
        Map wind = (Map) body.get("wind");

        WeatherDto dto = WeatherDto.builder()
            .name((String) body.get("name"))
            .description((String) weatherList.get(0).get("description"))
            .icon((String) weatherList.get(0).get("icon"))
            .temp(((Number) main.get("temp")).doubleValue())
            .feelsLike(((Number) main.get("feels_like")).doubleValue())
            .humidity(((Number) main.get("humidity")).intValue())
            .windSpeed(((Number) wind.get("speed")).doubleValue())
            .build();

        return ResponseEntity.ok(dto);
    }
}

โœ… WeatherDto.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WeatherDto {
    private String name;
    private String description;
    private String icon;
    private double temp;
    private double feelsLike;
    private int humidity;
    private double windSpeed;
}

๐Ÿ”ฅ ํ•ต์‹ฌ ์ •๋ฆฌ

ํ•ญ๋ชฉ์„ค๋ช…
์ขŒํ‘œ๊ณ„EPSG:5181 (Daum์šฉ)
์ง€๋„ APILeaflet + KoreanTmsProviders
๋‚ ์”จ APIOpenWeatherMap
์‹œ๊ฐํ™”๋งˆ์ปค + ํˆดํŒ + ํŒ์—…
๊ธฐ๋Šฅ ๊ฐœ์„ ์˜จ๋„ K โ†’ โ„ƒ, ์•„์ด์ฝ˜ ํ‘œ์‹œ, ๋งˆ์ปค ์ค‘๋ณต ์ œ๊ฑฐ, ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
๊ตฌ์กฐAxios โ†’ Spring REST โ†’ OpenWeather โ†’ DTO โ†’ ํŒ์—… ํ‘œ์‹œ

๐Ÿค” ๋А๋‚€ ์ 

  • ์ขŒํ‘œ๊ณ„ ์ •์˜๋ถ€ํ„ฐ API ํ˜ธ์ถœ, ์‘๋‹ต ํŒŒ์‹ฑ๊นŒ์ง€ ํ”„๋ก ํŠธ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ํ˜‘๋ ฅํ•˜๋Š” ์ „์ฒด ํ๋ฆ„์„ ์ฒดํ—˜ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • RestTemplate๋กœ ์™ธ๋ถ€ API๋ฅผ ์—ฐ๋™ํ•˜๊ณ , ํ•„์š”ํ•œ JSON ํ•„๋“œ๋งŒ ์ถ”์ถœํ•ด DTO๋กœ ์ •๋ฆฌํ•˜๋Š” ๊ฒฝํ—˜์ด ์œ ์ตํ–ˆ๋‹ค.
  • Leaflet์—์„œ ์ปค์Šคํ…€ ์ขŒํ‘œ๊ณ„๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฃผ์˜ํ•  ์ ๊ณผ ์‹ค์ œ ํƒ€์ผ ์†Œ์Šค๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๐Ÿ’ก ๋‹ค์Œ์— ๋„์ „ํ•ด๋ณผ ๊ฒƒ๋“ค

  • ์‚ฌ์šฉ์ž์˜ ํ˜„์žฌ ์œ„์น˜ ์ž๋™ ๋งˆ์ปค ํ‘œ์‹œ
  • ํด๋ฆญ ์œ„์น˜์˜ ๊ณผ๊ฑฐ ๋˜๋Š” ์ฃผ๊ฐ„ ๋‚ ์”จ ์ •๋ณด๋„ ํ•จ๊ป˜ ๋ณด์—ฌ์ฃผ๊ธฐ
  • ๋‚ ์”จ์— ๋”ฐ๋ผ ๋งˆ์ปค ์ƒ‰์ƒ์„ ๋™์ ์œผ๋กœ ๋ณ€๊ฒฝ
  • ๋‚ ์”จ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ์•„์ด์ฝ˜/๋ฐฐ๊ฒฝ์ƒ‰ ๋ฐ”๊พธ๋Š” UI ๊ฐœ์„ 

profile
Here, My Pale Blue.๐ŸŒ

0๊ฐœ์˜ ๋Œ“๊ธ€