Skip to content

Commit ad0204b

Browse files
authored
Merge pull request #181 from SEMOSAN/feat/#180-hiking-temperature
[Feat] 등산 기록 상세 조회에 등산 당시 기온 정보 추가
2 parents f47a57d + e8ce885 commit ad0204b

8 files changed

Lines changed: 171 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/java/com/semosan/api/domain/hiking/dto/response/HikingRecordDetailResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public record HikingRecordDetailResponse(
2525
Double ascentMeters,
2626
Double descentMeters,
2727
Integer calories,
28+
Double temperature,
2829
LocalDateTime startedAt,
2930
LocalDateTime endedAt,
3031
@JsonRawValue String track,
@@ -84,6 +85,7 @@ public static HikingRecordDetailResponse of(
8485
record.getAscent(),
8586
record.getDescent(),
8687
record.getCalories(),
88+
record.getTemperature(),
8789
record.getStartedAt(),
8890
record.getEndedAt(),
8991
track,

src/main/java/com/semosan/api/domain/hiking/entity/HikingRecord.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public class HikingRecord extends BaseEntity {
7777
@Column(name = "ended_at")
7878
private LocalDateTime endedAt;
7979

80+
@Column(name = "temperature")
81+
private Double temperature;
82+
8083
@Column(name = "clive_image_url", length = 500)
8184
private String cliveImageUrl;
8285

@@ -115,6 +118,10 @@ public static HikingRecord fromTrackingSession(
115118
.build();
116119
}
117120

121+
public void updateTemperature(Double temperature) {
122+
this.temperature = temperature;
123+
}
124+
118125
private static int computeDurationSeconds(TrackingSession session) {
119126
if (session.getStartedAt() == null || session.getEndedAt() == null) {
120127
return 0;

src/main/java/com/semosan/api/domain/tracking/service/TrackingSessionService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.semosan.api.common.exception.ConstraintViolationUtils;
2020
import com.semosan.api.domain.user.entity.User;
2121
import com.semosan.api.domain.user.service.UserReader;
22+
import com.semosan.api.common.weather.WeatherService;
2223
import lombok.RequiredArgsConstructor;
2324
import lombok.extern.slf4j.Slf4j;
2425
import org.springframework.context.ApplicationEventPublisher;
@@ -45,6 +46,7 @@ public class TrackingSessionService {
4546
private final HikingMemberRepository hikingMemberRepository;
4647
private final TrackingMilestoneTriggerService milestoneTriggerService;
4748
private final ApplicationEventPublisher eventPublisher;
49+
private final WeatherService weatherService;
4850

4951
/**
5052
* 세션 생성. 유저당 진행 중 세션은 1개만 허용한다.
@@ -128,6 +130,12 @@ public TrackingSessionResponse complete(Long userId, Long sessionId) {
128130
stats.ascentMeters(),
129131
stats.descentMeters()
130132
);
133+
Mountain mountain = session.getMountain();
134+
if (mountain.getLatitude() != null && mountain.getLongitude() != null) {
135+
weatherService.getTemperature(mountain.getLatitude(), mountain.getLongitude())
136+
.ifPresent(record::updateTemperature);
137+
}
138+
131139
HikingRecord savedRecord = hikingRecordRepository.save(record);
132140

133141
HikingMember member = HikingMember.create(savedRecord, session.getUser());

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}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE hiking_records
2+
ADD COLUMN temperature DOUBLE PRECISION;

0 commit comments

Comments
 (0)