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

[Backen] feat : 날씨 정보 가져오는 API #88

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.isp.backend.domain.country.controller;

import com.isp.backend.domain.country.dto.request.LocationRequest;
import com.isp.backend.domain.country.dto.response.DailyWeatherResponse;
import com.isp.backend.domain.country.dto.response.LocationResponse;
import com.isp.backend.domain.country.dto.response.WeatherResponse;
import com.isp.backend.domain.country.service.CountryService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalTime;
import java.util.List;
import java.util.Optional;

@RestController
Expand All @@ -32,4 +36,24 @@ public ResponseEntity<LocationResponse> findLocation(@RequestBody LocationReques
}


/** 현재 날씨 정보 가져오기 **/
@GetMapping("/weather/current")
public ResponseEntity<WeatherResponse> getCurrentWeather(@RequestBody LocationRequest requestDTO) {
String city = requestDTO.getCountry();
WeatherResponse weatherResponse = countryService.getCurrentWeather(city);
return ResponseEntity.ok(weatherResponse);
}


/** 한 주 날씨 정보 조회 **/
@GetMapping("/weather/weekly")
public ResponseEntity<List<DailyWeatherResponse>> getWeeklyWeather(@RequestBody LocationRequest requestDTO) {
String city = requestDTO.getCountry();
LocalTime requestedTime = LocalTime.of(12, 0);
List<DailyWeatherResponse> weeklyWeather = countryService.getWeeklyWeather(city, requestedTime);
return ResponseEntity.ok(weeklyWeather);
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.isp.backend.domain.country.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class DailyWeatherResponse {

private String date;

private String temp;

private String iconUrl;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.isp.backend.domain.country.dto.response;

import lombok.Data;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;


@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class WeatherResponse {

private String main;
private String description;

@JsonProperty("temp_min")
private String tempMin;

@JsonProperty("temp_max")
private String tempMax;

private String temp;

private String localTime;

private String iconUrl ;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.isp.backend.domain.country.mapper;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.InputStream;

/** weather id 값을 통해 description 출력 **/
public class WeatherMapper {

private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String MAPPING_FILE_PATH = "/weather_mapping.json";

public static String getWeatherDescriptionTranslation(String descriptionId) {
try {
InputStream inputStream = WeatherMapper.class.getResourceAsStream(MAPPING_FILE_PATH);
JsonNode mappingNode = objectMapper.readTree(inputStream);
JsonNode descriptionNode = mappingNode.get(descriptionId);

// 만약 해당 ID에 대한 설명이 없다면 기본값 반환 --> 추후 공통 에러 반환
if (descriptionNode == null) {
return "해당 설명을 찾을 수 없습니다.";
}
return descriptionNode.asText();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Failed to read weather mapping file", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package com.isp.backend.domain.country.service;


import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.isp.backend.domain.country.dto.response.DailyWeatherResponse;
import com.isp.backend.domain.country.dto.response.LocationResponse;
import com.isp.backend.domain.country.dto.response.WeatherResponse;
import com.isp.backend.domain.country.entity.Country;
import com.isp.backend.domain.country.mapper.WeatherMapper;
import com.isp.backend.domain.country.repository.CountryRepository;
import com.isp.backend.global.exception.openApi.OpenWeatherSearchFailedException;
import com.isp.backend.global.exception.schedule.CountryNotFoundException;
import com.isp.backend.global.s3.constant.S3BucketDirectory;
import com.isp.backend.global.s3.service.S3ImageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Optional;
import java.io.IOException;
import java.text.DecimalFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.*;


@Service
Expand All @@ -17,19 +32,171 @@ public class CountryService {
@Autowired
private CountryRepository countryRepository;

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

private final RestTemplate restTemplate = new RestTemplate();

@Autowired
private S3ImageService s3ImageService;


/** 여행지 좌표 찾기 **/
public Optional<LocationResponse> findLocationByCity(String city) {
Optional<Country> findCountry = countryRepository.findIdByCity(city);
return findCountry.map(country -> {
LocationResponse locationDTO = new LocationResponse();
locationDTO.setLatitude(country.getLatitude());
locationDTO.setLongitude(country.getLongitude());
return locationDTO;
});

if (findCountry.isPresent()) {
return findCountry.map(country -> {
LocationResponse locationDTO = new LocationResponse();
locationDTO.setLatitude(country.getLatitude());
locationDTO.setLongitude(country.getLongitude());
return locationDTO;
});
} else {
throw new CountryNotFoundException();
}
}


}
/** 여행지의 날씨 정보 가져오기 **/
public WeatherResponse getCurrentWeather(String city) {
Optional<Country> findCountry = findCountryByCity(city);
Country country = findCountry.get();
double latitude = country.getLatitude();
double longitude = country.getLongitude();

String url = String.format("http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s", latitude, longitude, apiKey);
String jsonResponse = restTemplate.getForObject(url, String.class);

ObjectMapper objectMapper = new ObjectMapper();
WeatherResponse weatherResponse = new WeatherResponse();

try {
JsonNode rootNode = objectMapper.readTree(jsonResponse);

JsonNode weatherNode = rootNode.path("weather").get(0);
weatherResponse.setMain(weatherNode.path("main").asText());
String descriptionId = weatherNode.path("id").asText(); // ID 값 가져오기
String descriptionTranslation = WeatherMapper.getWeatherDescriptionTranslation(descriptionId); // 날씨 설명
weatherResponse.setDescription(descriptionTranslation);

String icon = weatherNode.path("icon").asText();
weatherResponse.setIconUrl(s3ImageService.get(S3BucketDirectory.WEATHER.getDirectory(), icon + ".png"));

JsonNode mainNode = rootNode.path("main");
weatherResponse.setTemp(convertToCelsius(mainNode.path("temp").asDouble()));
weatherResponse.setTempMin(convertToCelsius(mainNode.path("temp_min").asDouble()));
weatherResponse.setTempMax(convertToCelsius(mainNode.path("temp_max").asDouble()));

int timezoneOffset = rootNode.path("timezone").asInt();
weatherResponse.setLocalTime(getLocalTime(timezoneOffset));

} catch (IOException e) {
e.printStackTrace();
throw new OpenWeatherSearchFailedException();
}

return weatherResponse;
}


/** 한주의 날씨 정보 조회 **/
public List<DailyWeatherResponse> getWeeklyWeather(String city, LocalTime requestedTime) {
Optional<Country> findCountry = findCountryByCity(city);
Country country = findCountry.get();
double latitude = country.getLatitude();
double longitude = country.getLongitude();

List<DailyWeatherResponse> weeklyWeather = new ArrayList<>();
String url = String.format("http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&appid=%s", latitude, longitude, apiKey);
String jsonResponse = restTemplate.getForObject(url, String.class);

try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(jsonResponse);
JsonNode forecastList = rootNode.path("list");

Map<String, List<Double>> dailyTemperatures = new HashMap<>(); // 각 날짜의 온도 목록을 저장할 맵

for (JsonNode forecast : forecastList) {
String dateTime = forecast.path("dt_txt").asText(); // 예측 시간 정보
LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

// 요청된 시간대의 온도만 고려
if (localDateTime.toLocalTime().equals(requestedTime)) {
String dateString = localDateTime.toLocalDate().toString();
double temperature = forecast.path("main").path("temp").asDouble(); // 온도 정보

// 해당 날짜의 온도 목록에 추가
dailyTemperatures.computeIfAbsent(dateString, k -> new ArrayList<>()).add(temperature);
}
}

// 각 날짜의 평균 온도를 계산하여 DailyWeatherResponse 객체로 변환한 후 리스트에 추가
dailyTemperatures.forEach((dateString, temperatures) -> {
double avgTemperature = temperatures.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
String temperature = convertToCelsius(avgTemperature);

// 아이콘 URL 가져오기
String iconCode = forecastList.get(0).path("weather").get(0).path("icon").asText();
String iconUrl = String.format(s3ImageService.get(S3BucketDirectory.WEATHER.getDirectory(), iconCode + ".png"));

DailyWeatherResponse dailyWeather = new DailyWeatherResponse();
dailyWeather.setDate(parseDayOfWeek(dateString));
dailyWeather.setTemp(temperature);
dailyWeather.setIconUrl(iconUrl);

weeklyWeather.add(dailyWeather);
});

// 날짜 순서대로 정렬
weeklyWeather.sort(Comparator.comparing(DailyWeatherResponse::getDate));

} catch (IOException e) {
e.printStackTrace();
throw new OpenWeatherSearchFailedException();
}

return weeklyWeather;
}


/** 현지 시간으로 변환 **/
private String getLocalTime(int timezoneOffset) {
Instant now = Instant.now();
ZoneOffset offset = ZoneOffset.ofTotalSeconds(timezoneOffset);
LocalDateTime localDateTime = LocalDateTime.ofInstant(now, offset);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
return localDateTime.format(formatter);
}


/** 온도를 섭씨로 변환 **/
private String convertToCelsius(double kelvin) {
double celsius = kelvin - 273.15;
DecimalFormat df = new DecimalFormat("#.##");
return df.format(celsius);
}


/** 날짜 문자열에서 요일을 파싱 **/
private String parseDayOfWeek(String dateString) {
LocalDate localDate = LocalDate.parse(dateString);
DayOfWeek dayOfWeek = localDate.getDayOfWeek();
String weekDay = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault());
return (dateString + ", " + weekDay);
}


/** 도시이름으로 나라 찾기 **/
public Optional<Country> findCountryByCity(String city) {
Optional<Country> findCountry = countryRepository.findIdByCity(city);
if (!findCountry.isPresent()) {
throw new CountryNotFoundException();
}
return findCountry;
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import com.isp.backend.domain.flight.dto.response.FlightLikeResponse;
import com.isp.backend.domain.flight.service.FlightOfferService;
import com.isp.backend.domain.flight.dto.request.FlightSearchRequest;
import com.isp.backend.global.exception.flight.AmadeusSearchFailedException;
import com.isp.backend.global.exception.flight.SkyScannerGenerateFailedException;
import com.isp.backend.global.exception.openApi.AmadeusSearchFailedException;
import com.isp.backend.global.exception.openApi.SkyScannerGenerateFailedException;
import com.isp.backend.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import com.isp.backend.domain.flight.repository.FlightRepository;
import com.isp.backend.domain.member.entity.Member;
import com.isp.backend.domain.schedule.service.ScheduleService;
import com.isp.backend.global.exception.flight.FlightNotFoundException;
import com.isp.backend.global.exception.flight.NotYourFlightException;
import com.isp.backend.global.exception.openApi.FlightNotFoundException;
import com.isp.backend.global.exception.openApi.NotYourFlightException;
import com.isp.backend.global.exception.schedule.CountryNotFoundException;
import com.isp.backend.global.exception.schedule.IataCodeNotFoundException;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.amadeus.exceptions.ResponseException;
import com.isp.backend.domain.hotel.dto.request.SearchGeocodeRequest;
import com.isp.backend.domain.hotel.service.HotelService;
import com.isp.backend.global.exception.flight.AmadeusSearchFailedException;
import com.isp.backend.global.exception.openApi.AmadeusSearchFailedException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class FastestScheduleResponse {

private String dday;

private String country ;

private String imageUrl;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.isp.backend.domain.country.entity.Country;
import com.isp.backend.domain.member.entity.Member;
import com.isp.backend.domain.scheduleDetail.entity.CheckList;
import com.isp.backend.domain.scheduleDetail.entity.ScheduleDetail;
import com.isp.backend.global.common.BaseEntity;
import jakarta.persistence.*;
Expand Down Expand Up @@ -49,5 +50,8 @@ public class Schedule extends BaseEntity {
@Column(nullable = false)
private boolean activated = true;

@OneToMany(mappedBy = "schedule", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CheckList> checkLists = new ArrayList<>();


}
Loading
Loading