diff --git a/backend/src/main/java/com/isp/backend/domain/country/controller/CountryController.java b/backend/src/main/java/com/isp/backend/domain/country/controller/CountryController.java index 67e585dd..dbd4fe3a 100644 --- a/backend/src/main/java/com/isp/backend/domain/country/controller/CountryController.java +++ b/backend/src/main/java/com/isp/backend/domain/country/controller/CountryController.java @@ -2,8 +2,10 @@ 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.ExchangeRateResponse; 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.ExchangeRate; import com.isp.backend.domain.country.service.CountryService; import com.isp.backend.domain.country.service.ExchangeRateService; import lombok.RequiredArgsConstructor; @@ -58,24 +60,23 @@ public ResponseEntity> getWeeklyWeather(@RequestBody } - /** 특정 기준 통화에 대한 모든 환율 정보 API **/ -// @GetMapping("/exchange-rates") -// public ResponseEntity getExchangeRates(@RequestParam String baseCurrency) { -// logger.info("Received request for exchange rates with base currency: {}", baseCurrency); -// -// try { -// Map exchangeRates = exchangeRateService.getExchangeRates(baseCurrency); -// return ResponseEntity.ok(exchangeRates); -// } catch (Exception e) { -// logger.error("Error fetching exchange rates", e); -// return ResponseEntity.badRequest().body(Map.of( -// "errorMessage", "환율 정보를 가져오는데 실패했습니다.", -// "httpStatus", "BAD_REQUEST", -// "code", null -// )); -// } -// } + /** 환율 정보 업데이트 API **/ + @GetMapping("/exchange-rates/update") + public String updateExchangeRate() { + try { + exchangeRateService.updateExchangeRates(); + return "updated Successfully"; + } catch (Exception e) { + return "failed to update exchange rates: " + e.getMessage(); + } + } + /** 환율 정보 조회 API **/ + @GetMapping("/exchange-rates") + public ResponseEntity> getAllExchangeRates() { + List exchangeRates = exchangeRateService.getAllExchangeRates(); + return ResponseEntity.ok(exchangeRates); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/domain/country/dto/response/ExchangeRateResponse.java b/backend/src/main/java/com/isp/backend/domain/country/dto/response/ExchangeRateResponse.java new file mode 100644 index 00000000..a766cec8 --- /dev/null +++ b/backend/src/main/java/com/isp/backend/domain/country/dto/response/ExchangeRateResponse.java @@ -0,0 +1,18 @@ +package com.isp.backend.domain.country.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ExchangeRateResponse { + private String baseCurrency; + private String targetCurrency; + private BigDecimal rate; +} diff --git a/backend/src/main/java/com/isp/backend/domain/country/entity/ExchangeRate.java b/backend/src/main/java/com/isp/backend/domain/country/entity/ExchangeRate.java index 228bce07..8b5e974e 100644 --- a/backend/src/main/java/com/isp/backend/domain/country/entity/ExchangeRate.java +++ b/backend/src/main/java/com/isp/backend/domain/country/entity/ExchangeRate.java @@ -2,18 +2,16 @@ import com.isp.backend.global.common.BaseEntity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; + import java.math.BigDecimal; @Getter +@Setter @AllArgsConstructor -@Entity -@Builder @NoArgsConstructor +@Entity @Table(name = "exchange_rate") public class ExchangeRate extends BaseEntity { @@ -21,10 +19,16 @@ public class ExchangeRate extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String fromCurrency; + private String baseCurrency; + + private String targetCurrency; - private String toCurrency; + private Double rate; - private BigDecimal rate; + public ExchangeRate(String baseCurrency, String targetCurrency, Double rate) { + this.baseCurrency = baseCurrency; + this.targetCurrency = targetCurrency; + this.rate = rate; + } } \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/domain/country/mapper/ExchangeRateMapper.java b/backend/src/main/java/com/isp/backend/domain/country/mapper/ExchangeRateMapper.java new file mode 100644 index 00000000..d082e329 --- /dev/null +++ b/backend/src/main/java/com/isp/backend/domain/country/mapper/ExchangeRateMapper.java @@ -0,0 +1,22 @@ +package com.isp.backend.domain.country.mapper; + +import com.isp.backend.domain.country.dto.response.ExchangeRateResponse; +import com.isp.backend.domain.country.entity.ExchangeRate; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +@RequiredArgsConstructor +public class ExchangeRateMapper { + + public ExchangeRateResponse convertToDto(ExchangeRate exchangeRate) { + ExchangeRateResponse dto = new ExchangeRateResponse(); + dto.setBaseCurrency(exchangeRate.getBaseCurrency()); + dto.setTargetCurrency(exchangeRate.getTargetCurrency()); + dto.setRate(BigDecimal.valueOf(exchangeRate.getRate())); + return dto; + } + +} diff --git a/backend/src/main/java/com/isp/backend/domain/country/repository/ExchageRateRespository.java b/backend/src/main/java/com/isp/backend/domain/country/repository/ExchageRateRespository.java deleted file mode 100644 index b79f39d0..00000000 --- a/backend/src/main/java/com/isp/backend/domain/country/repository/ExchageRateRespository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.isp.backend.domain.country.repository; - -import com.isp.backend.domain.country.entity.ExchangeRate; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ExchageRateRespository extends JpaRepository { - -} diff --git a/backend/src/main/java/com/isp/backend/domain/country/repository/ExchangeRateRepository.java b/backend/src/main/java/com/isp/backend/domain/country/repository/ExchangeRateRepository.java new file mode 100644 index 00000000..aa4381d3 --- /dev/null +++ b/backend/src/main/java/com/isp/backend/domain/country/repository/ExchangeRateRepository.java @@ -0,0 +1,9 @@ +package com.isp.backend.domain.country.repository; + +import com.isp.backend.domain.country.entity.ExchangeRate; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ExchangeRateRepository extends JpaRepository { + ExchangeRate findByBaseCurrencyAndTargetCurrency(String baseCurrency, String targetCurrency); + +} diff --git a/backend/src/main/java/com/isp/backend/domain/country/service/ExchangeRateService.java b/backend/src/main/java/com/isp/backend/domain/country/service/ExchangeRateService.java index c2619055..047d1cd7 100644 --- a/backend/src/main/java/com/isp/backend/domain/country/service/ExchangeRateService.java +++ b/backend/src/main/java/com/isp/backend/domain/country/service/ExchangeRateService.java @@ -1,56 +1,116 @@ package com.isp.backend.domain.country.service; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.isp.backend.domain.country.dto.response.ExchangeRateResponse; +import com.isp.backend.domain.country.entity.ExchangeRate; +import com.isp.backend.domain.country.mapper.ExchangeRateMapper; +import com.isp.backend.domain.country.repository.ExchangeRateRepository; +import com.isp.backend.global.exception.openApi.ExchangeRateIsFailedException; +import com.isp.backend.global.exception.openApi.ExchangeRateSearchFailedException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; +import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; -import java.util.HashMap; -import java.util.Map; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service public class ExchangeRateService { + private final ExchangeRateRepository exchangeRateRepository; + private final ExchangeRateMapper exchangeRateMapper ; @Value("${api-key.exchange-rate}") private String exchangeRateApiKey; - private final String BASE_URL = "https://v6.exchangerate-api.com/v6"; - - /** 특정 기준 통화에 대한 모든 환율 정보 가져오기 **/ -// public Map getExchangeRates(String baseCurrency) { -// String url = BASE_URL + exchangeRateApiKey + "/latest/" + baseCurrency; -// logger.info("Requesting exchange rates for base currency: {}", baseCurrency); -// -// try { -// String jsonResponse = restTemplate.getForObject(url, String.class); -// ObjectMapper objectMapper = new ObjectMapper(); -// JsonNode rootNode = objectMapper.readTree(jsonResponse); -// logger.info("Received response: {}", jsonResponse); -// -// String result = rootNode.path("result").asText(); -// if (!"success".equals(result)) { -// throw new RuntimeException("API 요청 실패: " + result); -// } -// -// JsonNode conversionRates = rootNode.path("conversion_rates"); -// Map rates = new HashMap<>(); -// conversionRates.fields().forEachRemaining(entry -> { -// rates.put(entry.getKey(), entry.getValue().asDouble()); -// }); -// -// return rates; -// } catch (IOException e) { -// logger.error("Error parsing JSON response", e); -// throw new RuntimeException("환율 정보를 가져오는데 실패했습니다.", e); -// } -// } + @Autowired + public ExchangeRateService(ExchangeRateRepository exchangeRateRepository, ExchangeRateMapper exchangeRateMapper) { + this.exchangeRateRepository = exchangeRateRepository; + this.exchangeRateMapper = exchangeRateMapper; + } + + /** 환율 데이터 가져와서 업데이트 하는 API 메서드 **/ + @Transactional + public void updateExchangeRates() { + // 한국과 미국 통화 환율 비율 저장 + updateRatesForBaseCurrency("KRW"); + updateRatesForBaseCurrency("USD"); + } + + + /** 환율 데이터 가져와서 업데이트 하는 API 메서드 **/ + public List getAllExchangeRates() { + try { + // DB에서 모든 환율 데이터를 가져옴 + List exchangeRates = exchangeRateRepository.findAll(); + + // baseCurrency가 "KRW" 또는 "USD"인 데이터만 필터링하고 DTO로 변환 + return exchangeRates.stream() + .filter(rate -> ("KRW".equals(rate.getBaseCurrency()) || "USD".equals(rate.getBaseCurrency())) + && !rate.getBaseCurrency().equals(rate.getTargetCurrency())) + .map(exchangeRateMapper::convertToDto) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new ExchangeRateIsFailedException() ; + } + } + + + /** 측정 baseCurrency 통화에 대해 업데이트 하는 메서드**/ + private void updateRatesForBaseCurrency(String baseCurrency) { + String exchangeRateAPI_URL = "https://v6.exchangerate-api.com/v6/" + exchangeRateApiKey + "/latest/" + baseCurrency; + + try { + URL url = new URL(exchangeRateAPI_URL); + HttpURLConnection request = (HttpURLConnection) url.openConnection(); + request.connect(); + + // API 응답 데이터를 JsonObject로 변환 + Gson gson = new Gson(); + JsonObject jsonobj = gson.fromJson(new InputStreamReader(request.getInputStream()), JsonObject.class); + + // API 응답 결과 + String req_result = jsonobj.get("result").getAsString(); + + if ("success".equals(req_result)) { + JsonObject conversionRates = jsonobj.getAsJsonObject("conversion_rates"); + for (String targetCurrency : conversionRates.keySet()) { + // 필요한 통화만 가져온다 + if (isSupportedCurrency(targetCurrency)) { + double rate = conversionRates.get(targetCurrency).getAsDouble(); + + // DB에서 환율 데이터 존재 여부 확인 + ExchangeRate existingRate = exchangeRateRepository.findByBaseCurrencyAndTargetCurrency(baseCurrency, targetCurrency); + + if (existingRate == null) { + ExchangeRate newExchangeRate = new ExchangeRate(baseCurrency, targetCurrency, rate); + exchangeRateRepository.save(newExchangeRate); + } else { + // 기록이 이미 존재하면, rate만 수정한다. + existingRate.setRate(rate); + exchangeRateRepository.save(existingRate); + } + } + } + } else { + throw new ExchangeRateSearchFailedException(); + } + } catch (Exception e) { + throw new ExchangeRateIsFailedException() ; + } + } + + /** 지원하는 통화만 가져오는 메서드 **/ + private boolean isSupportedCurrency(String currencyCode) { + return Set.of("JPY", "GBP", "EURO", "CHF", "CZK", "USD", "SGD", "TWD", "LAK", "MYR", "VND", "THB", "IDR", "PHP", "KRW").contains(currencyCode); + } + diff --git a/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java b/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java index 1b64695c..5d7e1037 100644 --- a/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java +++ b/backend/src/main/java/com/isp/backend/global/exception/ErrorCode.java @@ -32,7 +32,9 @@ public enum ErrorCode { SKY_SCANNER_GENERATE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"F002", "스카이스캐너 URL을 생성할 수 없습니다."), FLIGHT_NOT_FOUND(HttpStatus.NOT_FOUND, "F003", "해당 id의 항공권을 찾을 수 없습니다."), NOT_YOUR_FLIGHT(HttpStatus.UNAUTHORIZED, "F004", "사용자의 항공권이 아닙니다"), - OPEN_WEATHER_SEARCH_FAILED(HttpStatus.NOT_FOUND,"F005", "날씨 정보 파싱에 실패하였습니다"); + OPEN_WEATHER_SEARCH_FAILED(HttpStatus.NOT_FOUND,"F005", "날씨 정보 파싱에 실패하였습니다"), + EXCHANGE_RATE_SEARCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"F006", "환율 요청을 가져오는 중 오류를 발생했습니다."), + EXCHANGE_RATE_IS_FAILED(HttpStatus.BAD_REQUEST,"F007", "환율 DB를 저장하던 중 오류가 발생하였습니다."); private HttpStatus status; diff --git a/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateIsFailedException.java b/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateIsFailedException.java new file mode 100644 index 00000000..084a7b6c --- /dev/null +++ b/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateIsFailedException.java @@ -0,0 +1,12 @@ +package com.isp.backend.global.exception.openApi; + +import com.isp.backend.global.exception.CustomException; +import com.isp.backend.global.exception.ErrorCode; + +public class ExchangeRateIsFailedException extends CustomException { + + public ExchangeRateIsFailedException() { + super(ErrorCode.EXCHANGE_RATE_IS_FAILED); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateSearchFailedException.java b/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateSearchFailedException.java new file mode 100644 index 00000000..8704f299 --- /dev/null +++ b/backend/src/main/java/com/isp/backend/global/exception/openApi/ExchangeRateSearchFailedException.java @@ -0,0 +1,12 @@ +package com.isp.backend.global.exception.openApi; + +import com.isp.backend.global.exception.CustomException; +import com.isp.backend.global.exception.ErrorCode; + +public class ExchangeRateSearchFailedException extends CustomException { + + public ExchangeRateSearchFailedException() { + super(ErrorCode.EXCHANGE_RATE_SEARCH_FAILED); + } + +} \ No newline at end of file