Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 게시글 생성 시 특정 도시의 현재 기온, 날씨 아이콘을 함께 저장하도록 구현 #23

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions aboutTime-application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
package com.aboutTime.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

@Configuration
public class ClientConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8))
.build();
public RestTemplate restTemplate() {
return new RestTemplate();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public class PostDto {
private String mainComment;
private Long authorId;

private Double currentTemperature = 29.5;
private String weatherIcon = "http://openweathermap.org/img/wn/01d@2x.png";
private Double currentTemperature;
private String weatherIcon;

private List<PostImageDto> postImages;
private List<Reaction> reactions = new ArrayList<>();
Expand All @@ -53,8 +53,8 @@ public static PostDto specificForm(Post post) {
post.getMainImage(),
post.getMainComment(),
post.getAuthorId(),
29.5,
"http://openweathermap.org/img/wn/01d@2x.png",
post.getCurrentTemperature(),
post.getWeatherIcon(),
images,
new ArrayList<>()
);
Expand All @@ -67,8 +67,8 @@ public static PostDto simpleForm(Post post) {
post.getMainImage(),
post.getMainComment(),
post.getAuthorId(),
29.5,
"http://openweathermap.org/img/wn/01d@2x.png",
post.getCurrentTemperature(),
post.getWeatherIcon(),
null,
new ArrayList<>()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ public class PostSaveRequestDto {
private String mainComment;
private List<PostImageDto> postImages;

public Post toEntity(Long authorId) {
public Post toEntity(Long authorId, double temperature, String weatherIcon) {
return Post.builder()
.mainImage(mainImage)
.mainComment(mainComment)
.authorId(authorId)
.currentTemperature(29.5) //mocking 값
.weatherIcon("http://openweathermap.org/img/wn/01d@2x.png") //mocking 값
.currentTemperature(temperature)
.weatherIcon(weatherIcon)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.aboutTime.infra;

import com.aboutTime.entity.post.weather.Weather;
import com.aboutTime.entity.post.weather.WeatherData;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class WeatherAPIService {

private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;

private static final String QUERY_PARAM_CITY = "q";
private static final String QUERY_PARAM_APPID = "appid";
private static final String QUERY_PARAM_UNITS = "units";
private static final String UNITS_METRIC = "metric";

@Value("${weather.api.key}")
private String apiKey;

@Value("${weather.api.url}")
private String apiUrl;

public Weather getWeatherDataByCity(String city) {
String url = buildWeatherApiUrl(city);
String response = requestWeatherData(url);
JsonNode jsonNode = parseWeatherResponse(response);
return createWeatherData(city, jsonNode);
}

private String buildWeatherApiUrl(String city) {
return UriComponentsBuilder.fromHttpUrl(apiUrl)
.queryParam(QUERY_PARAM_CITY, city)
.queryParam(QUERY_PARAM_APPID, apiKey)
.queryParam(QUERY_PARAM_UNITS, UNITS_METRIC)
.toUriString();
}

private String requestWeatherData(String url) {
try {
return restTemplate.getForObject(url, String.class);
} catch (Exception e) {
throw new RuntimeException("날씨 데이터 요청 실패: " + e.getMessage(), e);
}
}

private JsonNode parseWeatherResponse(String response) {
try {
return objectMapper.readTree(response);
} catch (Exception e) {
throw new RuntimeException("날씨 데이터 파싱 실패: " + e.getMessage(), e);
}
}

private Weather createWeatherData(String city, JsonNode jsonNode) {
int conditionCode = extractConditionCode(jsonNode);
double temperature = extractTemperature(jsonNode);
int hour = getCurrentHour(jsonNode);
String iconName = WeatherData.fromConditionCode(conditionCode, hour);

return Weather.builder()
.city(city)
.conditionCode(iconName)
.temperature(temperature)
.hour(hour)
.build();
}

private int extractConditionCode(JsonNode jsonNode) {
return jsonNode.path("weather").get(0).path("id").asInt();
}

private double extractTemperature(JsonNode jsonNode) {
return jsonNode.path("main").path("temp").asDouble();
}

private int getCurrentHour(JsonNode jsonNode) {
long timestamp = jsonNode.path("dt").asLong();
int timezoneOffset = jsonNode.path("timezone").asInt();

return ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneOffset.UTC)
.withZoneSameInstant(ZoneOffset.ofTotalSeconds(timezoneOffset)).getHour();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.aboutTime.dto.post.PostSaveRequestDto;
import com.aboutTime.entity.User;
import com.aboutTime.entity.post.PostRepository;
import com.aboutTime.entity.post.weather.Weather;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,11 +19,9 @@
public class PostService {

private final PostRepository postRepository;
private final WeatherService weatherService;

public List<PostDto> getAllPostByUserId(Long userId) {
// return postRepository.findAllByAuthorId((user.getId())).stream()
// .map(PostDto::from)
// .toList();
return postRepository.findAllByAuthorId(userId).stream()
.map(PostDto::simpleForm)
.toList();
Expand All @@ -43,9 +42,13 @@ public PostDto getOnePostById(Long postId) {

@Transactional
public void save(PostSaveRequestDto postRequestDto, Long authorId) {
// var user = userRepository.findById(authorId)
// .orElseThrow(() -> new ResourceNotFoundException("해당하는 유저가 없습니다."));
var post = postRepository.save(postRequestDto.toEntity(authorId));
Weather weather = weatherService.getWeatherByCity("Seoul"); //mocking 값

double temperature = weather.getTemperature();
String conditionCode = weather.getConditionCode();
String weatherIcon = weatherService.getWeatherIconUrlByCode(conditionCode);

var post = postRepository.save(postRequestDto.toEntity(authorId, temperature, weatherIcon));

Objects.requireNonNull(postRequestDto.getPostImages()).stream()
.map(archiveImageDto -> archiveImageDto.toEntity(post))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.aboutTime.service.post;

import com.aboutTime.common.exception.ResourceNotFoundException;
import com.aboutTime.entity.post.weather.Weather;
import com.aboutTime.entity.post.weather.WeatherIcon;
import com.aboutTime.entity.post.weather.WeatherIconRepository;
import com.aboutTime.entity.post.weather.WeatherRepository;
import com.aboutTime.infra.WeatherAPIService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class WeatherService {

private final WeatherRepository weatherRepository;
private final WeatherAPIService weatherAPIService;
private final WeatherIconRepository weatherIconRepository;

//3시간마다 날씨 정보 업데이트
@Scheduled(cron = "0 0 */3 * * ?")
@Async
@Transactional
public void updateWeatherData() {
log.info("Weather data update start");
List<Weather> weatherList = weatherRepository.findAll();

for (Weather weather : weatherList) {
try {
updateWeather(weather);
} catch (Exception e) {
log.error("Failed to update weather for city: {} - Error: {}", weather.getCity(), e.getMessage());
}
}
log.info("Weather data updated Completed: {}", weatherList.size());
}

@Transactional
public Weather saveWeatherData(String city) {
Weather updatedWeather = weatherAPIService.getWeatherDataByCity(city);
return updateOrSaveWeather(city, updatedWeather);
}

public Weather getWeatherByCity(String city) {
return weatherRepository.findByCity(city)
.orElseThrow(() -> new ResourceNotFoundException(city + "에 해당하는 날씨 데이터가 없습니다."));
}

public List<Weather> getAllWeatherData() {
return weatherRepository.findAll();
}

private void updateWeather(Weather weather) {
String city = weather.getCity();
Weather updatedWeather = weatherAPIService.getWeatherDataByCity(city);
applyUpdatedWeatherData(weather, updatedWeather);
}

private Weather updateOrSaveWeather(String city, Weather updatedWeather) {
return weatherRepository.findByCity(city)
.map(existingWeather -> {
applyUpdatedWeatherData(existingWeather, updatedWeather);
log.info("Updated weather data for city: {}", city);
return weatherRepository.save(existingWeather);
})
.orElseGet(() -> {
log.info("Creating new weather data for city: {}", city);
return weatherRepository.save(updatedWeather);
});
}

private void applyUpdatedWeatherData(Weather existingWeather, Weather updatedWeather) {
existingWeather.setConditionCode(updatedWeather.getConditionCode());
existingWeather.setTemperature(updatedWeather.getTemperature());
existingWeather.setHour(updatedWeather.getHour());
}

public String getWeatherIconUrlByCode(String code) {
return weatherIconRepository.findByCode(code)
.map(WeatherIcon::getUrl)
.orElseThrow(() -> new ResourceNotFoundException("해당하는 날씨 아이콘이 없습니다."));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.aboutTime.entity.post.weather;

import jakarta.persistence.*;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "weather")
public class Weather {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Column(name = "city")
private String city;

// API의 응답값으로 주는 고유 코드
@Column(name = "condition_code")
private String conditionCode;

@Column(name = "temperature")
private double temperature;

@Column(name = "hour")
private int hour;

@Builder
public Weather(String city, String conditionCode, double temperature, int hour) {
this.city = city;
this.conditionCode = conditionCode;
this.temperature = temperature;
this.hour = hour;
}

}

Loading