Skip to content

Commit 06ada5e

Browse files
committed
feat: 기상청 초단기실황 API 연동 서비스 구현
1 parent dcc6161 commit 06ada5e

4 files changed

Lines changed: 152 additions & 0 deletions

File tree

src/main/java/com/semosan/api/common/config/WebClientConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ public WebClient appleApiWebClient() {
2121
.build();
2222
}
2323

24+
@Bean
25+
public WebClient weatherApiWebClient() {
26+
return WebClient.builder()
27+
.baseUrl("https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0")
28+
.build();
29+
}
30+
2431
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.semosan.api.common.util;
2+
3+
public class GridConverter {
4+
5+
private static final double RE = 6371.00877;
6+
private static final double GRID = 5.0;
7+
private static final double SLAT1 = 30.0;
8+
private static final double SLAT2 = 60.0;
9+
private static final double OLON = 126.0;
10+
private static final double OLAT = 38.0;
11+
private static final double XO = 43;
12+
private static final double YO = 136;
13+
14+
private GridConverter() {
15+
}
16+
17+
public record Grid(int nx, int ny) {
18+
}
19+
20+
public static Grid toGrid(double lat, double lon) {
21+
double degrad = Math.PI / 180.0;
22+
23+
double re = RE / GRID;
24+
double slat1 = SLAT1 * degrad;
25+
double slat2 = SLAT2 * degrad;
26+
double olon = OLON * degrad;
27+
double olat = OLAT * degrad;
28+
29+
double sn = Math.tan(Math.PI * 0.25 + slat2 * 0.5) / Math.tan(Math.PI * 0.25 + slat1 * 0.5);
30+
sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(sn);
31+
double sf = Math.tan(Math.PI * 0.25 + slat1 * 0.5);
32+
sf = Math.pow(sf, sn) * Math.cos(slat1) / sn;
33+
double ro = Math.tan(Math.PI * 0.25 + olat * 0.5);
34+
ro = re * sf / Math.pow(ro, sn);
35+
36+
double ra = Math.tan(Math.PI * 0.25 + lat * degrad * 0.5);
37+
ra = re * sf / Math.pow(ra, sn);
38+
double theta = lon * degrad - olon;
39+
if (theta > Math.PI) theta -= 2.0 * Math.PI;
40+
if (theta < -Math.PI) theta += 2.0 * Math.PI;
41+
theta *= sn;
42+
43+
int nx = (int) Math.floor(ra * Math.sin(theta) + XO + 0.5);
44+
int ny = (int) Math.floor(ro - ra * Math.cos(theta) + YO + 0.5);
45+
46+
return new Grid(nx, ny);
47+
}
48+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.semosan.api.common.weather;
2+
3+
import com.semosan.api.common.util.GridConverter;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.beans.factory.annotation.Qualifier;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.web.reactive.function.client.WebClient;
9+
10+
import java.time.Duration;
11+
import java.time.LocalDateTime;
12+
import java.time.format.DateTimeFormatter;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Optional;
16+
17+
@Slf4j
18+
@Service
19+
public class WeatherService {
20+
21+
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
22+
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HHmm");
23+
24+
private final WebClient webClient;
25+
private final String apiKey;
26+
27+
public WeatherService(
28+
@Qualifier("weatherApiWebClient") WebClient webClient,
29+
@Value("${weather.api-key}") String apiKey
30+
) {
31+
this.webClient = webClient;
32+
this.apiKey = apiKey;
33+
}
34+
35+
public Optional<Double> getTemperature(double latitude, double longitude) {
36+
try {
37+
GridConverter.Grid grid = GridConverter.toGrid(latitude, longitude);
38+
LocalDateTime now = adjustBaseTime(LocalDateTime.now());
39+
40+
String baseDate = now.format(DATE_FMT);
41+
String baseTime = now.format(TIME_FMT);
42+
43+
Map<String, Object> response = webClient.get()
44+
.uri(uriBuilder -> uriBuilder
45+
.path("/getUltraSrtNcst")
46+
.queryParam("serviceKey", apiKey)
47+
.queryParam("pageNo", 1)
48+
.queryParam("numOfRows", 10)
49+
.queryParam("dataType", "JSON")
50+
.queryParam("base_date", baseDate)
51+
.queryParam("base_time", baseTime)
52+
.queryParam("nx", grid.nx())
53+
.queryParam("ny", grid.ny())
54+
.build())
55+
.retrieve()
56+
.bodyToMono(Map.class)
57+
.block(Duration.ofSeconds(3));
58+
59+
return extractTemperature(response);
60+
} catch (Exception e) {
61+
log.warn("기상청 API 호출 실패: {}", e.getMessage());
62+
return Optional.empty();
63+
}
64+
}
65+
66+
@SuppressWarnings("unchecked")
67+
private Optional<Double> extractTemperature(Map<String, Object> response) {
68+
if (response == null) return Optional.empty();
69+
70+
Map<String, Object> root = (Map<String, Object>) response.get("response");
71+
if (root == null) return Optional.empty();
72+
73+
Map<String, Object> body = (Map<String, Object>) root.get("body");
74+
if (body == null) return Optional.empty();
75+
76+
Map<String, Object> items = (Map<String, Object>) body.get("items");
77+
if (items == null) return Optional.empty();
78+
79+
List<Map<String, Object>> itemList = (List<Map<String, Object>>) items.get("item");
80+
if (itemList == null) return Optional.empty();
81+
82+
return itemList.stream()
83+
.filter(item -> "T1H".equals(item.get("category")))
84+
.findFirst()
85+
.map(item -> Double.parseDouble(String.valueOf(item.get("obsrValue"))));
86+
}
87+
88+
private LocalDateTime adjustBaseTime(LocalDateTime now) {
89+
if (now.getMinute() < 40) {
90+
now = now.minusHours(1);
91+
}
92+
return now.withMinute(0).withSecond(0).withNano(0);
93+
}
94+
}

src/main/resources/application.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ management:
7979
export:
8080
enabled: true
8181

82+
weather:
83+
api-key: ${WEATHER_API_KEY}
84+
8285
discord:
8386
alert:
8487
enabled: ${DISCORD_ALERT_ENABLED}

0 commit comments

Comments
 (0)