1. 이전 포스팅

 

https://growingsaja.tistory.com/986

 

[Kotlin][SpringBoot3] Kopring 서버 기본 실습 15 - 외부 api를 활용해 Scheduler로 정보 수집 실습 2 : open excha

1. 이전 포스팅 https://growingsaja.tistory.com/981 [Kotlin][SpringBoot3] Kopring 서버 응용 실습 01 - 새로운 프로젝트 하면 좋은 사전 세팅 1. 이전 포스팅 https://growingsaja.tistory.com/984 2. 목표 실시간 환율 정보 및

growingsaja.tistory.com

 

 

 

 

 

2. 목표

 

 - isNeedUpdate 함수 결과 data class 기본 entity로 만들어두기

 - MongoDB에 Dunamu에서 제공해주는 api 활용해 주요 국가 KRW 기준 환율 가격 및 관련 정보 조회 및 수집 -> DunamuRateData

 - 주요 국가 환율 정보 최신화 -> FiatRateInfo

 

 

 

 

 

3. 정보 수집에 활용할 api 확인

 

 - 대상

"EUR", "JPY", "GBP", "CHF", "CAD", "AUD", "CNY", "HKD", "SEK", "NZD", "KRW", "SGD", "NOK", "MXN", "INR", "RUB", "ZAR", "TRY", "BRL", "AED", "BHD", "BND", "CNH", "CZK", "DKK", "IDR", "ILS", "MYR", "QAR", "SAR", "THB", "TWD", "CLP", "COP", "EGP", "HUF", "KWD", "OMR", "PHP", "PLN", "PKR", "RON", "BDT", "DZD", "ETB", "FJD", "JOD", "KES", "KHR", "KZT", "LKR", "LYD", "MMK", "MNT", "MOP", "NPR", "TZS", "UZS", "VND"

 

 - 1 USD 당 x KRW 환율 관련 정보

   일반적인 형태의 return 데이터입니다.

https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD
[
    {
        "code": "FRX.KRWUSD",
        "currencyCode": "USD",
        "currencyName": "달러",
        "country": "미국",
        "name": "미국 (USD/KRW)",
        "date": "2023-09-18",
        "time": "16:50:04",
        "recurrenceCount": 243,
        "basePrice": 1326.50,
        "openingPrice": 1327.60,
        "highPrice": 1329.50,
        "lowPrice": 1323.40,
        "change": "FALL",
        "changePrice": 4.50,
        "cashBuyingPrice": 1349.71,
        "cashSellingPrice": 1303.29,
        "ttBuyingPrice": 1313.60,
        "ttSellingPrice": 1339.40,
        "tcBuyingPrice": null,
        "fcSellingPrice": null,
        "exchangeCommission": 7.1771,
        "usDollarRate": 1.0000,
        "high52wPrice": 1444.00,
        "high52wDate": "2022-10-25",
        "low52wPrice": 1216.60,
        "low52wDate": "2023-02-02",
        "currencyUnit": 1,
        "provider": "하나은행",
        "timestamp": 1695023418513,
        "id": 79,
        "createdAt": "2016-10-21T06:13:34.000+00:00",
        "modifiedAt": "2023-09-18T07:50:19.000+00:00",
        "signedChangePrice": -4.50,
        "signedChangeRate": -0.0033809166,
        "changeRate": 0.0033809166
    }
]

 

 - 1 USD 당 x GBP 환율 관련 정보

   이 또한 일반적인 형태의 return 데이터입니다.

https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.GBPUSD
[
    {
        "code": "FRX.GBPUSD",
        "currencyCode": "GBP",
        "currencyName": "파운드",
        "country": "영국",
        "name": "영국 (USD/GBP)",
        "date": "2023-09-18",
        "time": "16:50:04",
        "recurrenceCount": 243,
        "basePrice": 0.81,
        "openingPrice": 0.81,
        "highPrice": 0.81,
        "lowPrice": 0.81,
        "change": "RISE",
        "changePrice": 0.01,
        "cashBuyingPrice": 1675.10,
        "cashSellingPrice": 1610.38,
        "ttBuyingPrice": 1626.32,
        "ttSellingPrice": 1659.16,
        "tcBuyingPrice": null,
        "fcSellingPrice": null,
        "exchangeCommission": 7.4560,
        "usDollarRate": 1.2384,
        "high52wPrice": 0.90,
        "high52wDate": "2022-11-04",
        "low52wPrice": 0.76,
        "low52wDate": "2023-07-19",
        "currencyUnit": 1,
        "provider": "하나은행",
        "timestamp": 1695023416892,
        "id": 28,
        "createdAt": "2016-10-21T06:13:30.000+00:00",
        "modifiedAt": "2023-09-18T07:50:17.000+00:00",
        "signedChangePrice": 0.01,
        "signedChangeRate": 0.0125,
        "changeRate": 0.0125
    }
]

 

 - 100 JPY 당 x KRW 환율 관련 정보

   해당 api처럼 1 JPY 당이 아닌, 100 JPY 당으로 return되는 경우가 있으므로 감안해서 개발해야합니다.

https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWJPY
[
    {
        "code": "FRX.KRWJPY",
        "currencyCode": "JPY",
        "currencyName": "엔",
        "country": "일본",
        "name": "일본 (JPY100/KRW)",
        "date": "2023-09-18",
        "time": "14:00:11",
        "recurrenceCount": 172,
        "basePrice": 897.78,
        "openingPrice": 897.91,
        "highPrice": 899.31,
        "lowPrice": 897.48,
        "change": "FALL",
        "changePrice": 3.68,
        "cashBuyingPrice": 913.49,
        "cashSellingPrice": 882.07,
        "ttBuyingPrice": 888.99,
        "ttSellingPrice": 906.57,
        "tcBuyingPrice": null,
        "fcSellingPrice": null,
        "exchangeCommission": 2.0414,
        "usDollarRate": 0.6772,
        "high52wPrice": 1008.90,
        "high52wDate": "2023-04-06",
        "low52wPrice": 895.18,
        "low52wDate": "2023-08-01",
        "currencyUnit": 100,
        "provider": "하나은행",
        "timestamp": 1695013225012,
        "id": 41,
        "createdAt": "2016-10-21T06:13:31.000+00:00",
        "modifiedAt": "2023-09-18T05:00:25.000+00:00",
        "signedChangePrice": -3.68,
        "changeRate": 0.0040822665,
        "signedChangeRate": -0.0040822665
    }
]

 

 - 1 USD 당 x LYD 환율 관련 정보

   해당 api처럼 일부 데이터가 null로 return되는 경우가 있으므로 감안해서 개발해야합니다.

     ㄴ currencyName

     ㄴ country

     ㄴ name

     ㄴ tcBuyingPrice

     ㄴ fcSellingPrice

     ㄴ provider

 

https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.LYDUSD
[
    {
        "code": "FRX.LYDUSD",
        "currencyCode": "LYD",
        "currencyName": null,
        "country": null,
        "name": null,
        "date": "2023-09-18",
        "time": "16:50:04",
        "recurrenceCount": 243,
        "basePrice": 4.85,
        "openingPrice": 4.86,
        "highPrice": 4.86,
        "lowPrice": 4.84,
        "change": "RISE",
        "changePrice": 0.01,
        "cashBuyingPrice": 0.00,
        "cashSellingPrice": 0.00,
        "ttBuyingPrice": 270.87,
        "ttSellingPrice": 276.33,
        "tcBuyingPrice": null,
        "fcSellingPrice": null,
        "exchangeCommission": 0.0000,
        "usDollarRate": 0.2063,
        "high52wPrice": 5.09,
        "high52wDate": "2022-10-17",
        "low52wPrice": 4.65,
        "low52wDate": "2023-02-06",
        "currencyUnit": 1,
        "provider": null,
        "timestamp": 1695023417463,
        "id": 109,
        "modifiedAt": "2023-09-18T07:50:17.000+00:00",
        "createdAt": "2022-09-01T12:22:41.000+00:00",
        "signedChangePrice": 0.01,
        "signedChangeRate": 0.0020661157,
        "changeRate": 0.0020661157
    }
]

 

 

 

 

 

5. null 데이터들 직접 채워주기

 

currencyCode 에 따른 currencyName 값 리스팅

"CNH"
"CLP"
"COP"
"OMR"
"RON"
"DZD"
"ETB"
"FJD"
"KES"
"KHR"
"LKR"
"LYD"
"MMK"
"MOP"
"NPR"
"TZS"
"UZS"

"위안"
"페소"
"페소"
"리알"
"레위"
"디나르"
"비르"
"달러"
"실링"
"리얄"
"루피"
"디나르"
"키얏"
"파타카"
"루피"
"실링"
"솜"

 

currencyCode 에 따른 countryName 값 리스팅

"CNH"
"CLP"
"COP"
"OMR"
"RON"
"DZD"
"ETB"
"FJD"
"KES"
"KHR"
"LKR"
"LYD"
"MMK"
"MOP"
"NPR"
"TZS"
"UZS"

"홍콩"
"칠레"
"콜롬비아"
"오만"
"루마니아"
"알제리"
"에티오피아"
"피지"
"케냐"
"캄보디아"
"스리랑카"
"리비아"
"미얀마"
"마카오"
"네팔"
"탄자니아"
"우즈베키스탄"

 

 - "대한민국"의 경우 "대한민국"이 아닌 "미국"이라고 기재되는 이슈 있음

 - "원"의 경우 "원"이 아닌 "달러"라고 기재되는 이슈 있음

    -> 한국에서 제공하는 api를 활용한 탓으로 추측됨. 각각 데이터 수정 저장 필요

 

 

 

 

 

6. 프로젝트 구조

 

 - IsNeedUpdateData는 data를 업데이트해도 문제가 없을지 확인하는 용도의 데이터 타입입니다. status와 data로 구성되어있습니다.

 

 

 

 

 

7. data class 만들기

 

// vim IsNeedUpdateData.kt

package com.dev.kopring00.base.entities

// data를 업데이트할지에 대한 여부를 return하는 isNeedUpdate 계열 함수에서 return 데이터로 사용합니다.
// 사용 이유 : 불필요한 DB call 반복 및 api call 반복을 예방하기 위해
data class IsNeedUpdateData(
    var status: Boolean,
    var data: String?
)

data는 안쓸수도 있으므로 nullable로 작성해줍니다. 추후 해당 data class 사용시 넘길 data가 없다면 null을 data에 넣어줍니다.

 

// vim DunamuRateData_raw.kt

package com.dev.kopring00.external.fiat.entities

data class DunamuRateData_raw(
    val code: String,   // 환율 코드 : FRX.KRWJPY -> KRWJPY
    val currencyCode: String,   // 통화 코드 : JPY
    val currencyName: String?,  // 통화 이름 : 엔
    val country: String?,   // 국가명 : 일본
    val name: String?,  // 환율의 이름 : 일본 (JPY100/KRW)
    val date: String,   // 정보의 기준 날짜
    val time: String,   // 정보의 기준 시간
    val recurrenceCount: Int,   // 환율 정보 업데이트 횟수 (하나은행 기준 환율 공시 차수)
    val basePrice: Double,  // 환율 기준 가격
    val openingPrice: Double,   // 오늘의 환율 시가
    val highPrice: Double,  // 오늘의 환율 고가
    val lowPrice: Double,   // 오늘의 환율 저가
    val cashBuyingPrice: Double,    // 현금으로 구매시 적용 환율 가격 Buy
    val cashSellingPrice: Double,   // 현금으로 판매시 적용 환율 가격 Sell
    val ttBuyingPrice: Double,      // 전신환 구매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Buy
    val ttSellingPrice: Double,     // 전신환 판매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Sell
    val tcBuyingPrice: Double?,     // 전자환 구매시 적용 환율 가격 Buy
    val fcSellingPrice: Double?,    // 전자환 판매시 적용 환율 가격 Sell
    val exchangeCommission: Double, // 환율 수수료 (계산 방식 불명...)
    val usDollarRate: Double,   // 미국 달러 기준 환율 : 100JPY = 0.6776USD
    val high52wPrice: Double,   // 최근 52주 최고가
    val high52wDate: String,    // 최근 52주 최고가 발생 일자
    val low52wPrice: Double,    // 최근 52주 최저가
    val low52wDate: String,     // 최근 52주 최저가 발생 일자
    val currencyUnit: Int,      // 통화 화폐 환율 단위 : 100
    val provider: String?,      // 정보 출처 : 하나은행 or null
    val timestamp: Long,        // api response return 시점의 dunamu api 서버 시간 unix timestamp
    val id: Short,               // 고유 식별값
    val signedChangePrice: Double,  // 환율 가격 변동률
    val signedChangeRate: Double,   // 환율 가격 변동가
    val change: String,         // 오늘의 환율 등락 여부 : EVEN 보합, RISE 상승, FALL 하락
    val changePrice: Double,    // 오늘의 환율 변동가 절대값 = unsignedChangePrice
    val changeRate: Double      // 오늘의 환율 변동률 절대값
)
// vim DunamuRateData.kt

package com.dev.kopring00.external.fiat.entities

import org.springframework.data.mongodb.core.mapping.Document

@Document(collection = "dunamuRateData")
data class DunamuRateData(
    val fullCode: String?,
    val timestamp: Long,   // 정보의 기준 일시 unix timestamp
    val basePrice: Double,  // 환율 기준 가격
    val openingPrice: Double,   // 오늘의 환율 시가 = 전일 종가
    val highPrice: Double,  // 오늘의 환율 고가 <- UTC 00시 기준
    val lowPrice: Double,   // 오늘의 환율 저가 <- UTC 00시 기준
    // 현금 환전
    val cashBuyingPrice: Double?,    // 현금으로 구매시 적용 환율 가격 Buy
    val cashSellingPrice: Double?,   // 현금으로 판매시 적용 환율 가격 Sell
    val cashBuyingFeeRate: Float?,    // 현금으로 구매시 환전 수수료율 Buy
    val cashBuyingFeePrice: Double?,    // 현금으로 구매시 환전 수수료 가격 Buy
    val cashSellingFeeRate: Float?,   // 현금으로 판매시 환전 수수료율 Sell
    val cashSellingFeePrice: Double?,   // 현금으로 판매시 환전 수수료 가격 Sell
    // 전신환 환전
    val ttBuyingPrice: Double?,      // 전신환 구매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Buy
    val ttSellingPrice: Double?,     // 전신환 판매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Sell
    val ttBuyingFeeRate: Float?,      // 전신환 구매시 환전 수수료율 (전신환 기관 간의 환전시 적용) Buy
    val ttBuyingFeePrice: Double?,      // 전신환 구매시 환전 수수료 가격 (전신환 기관 간의 환전시 적용) Buy
    val ttSellingFeeRate: Float?,     // 전신환 판매시 환전 수수료율 (전신환 기관 간의 환전시 적용) Sell
    val ttSellingFeePrice: Double?,     // 전신환 판매시 환전 수수료 가격 (전신환 기관 간의 환전시 적용) Sell

    val usDollarRate: Double,   // 미국 달러 기준 환율 : 100JPY = 0.6776USD
    // 최근 52주 관련 정보
    val high52wPrice: Double,   // 최근 52주 최고가
    val high52wDate: String,    // 최근 52주 최고가 발생 일자
    val low52wPrice: Double,    // 최근 52주 최저가
    val low52wDate: String,     // 최근 52주 최저가 발생 일자

    val provider: String?,      // 정보 출처 : 하나은행 or null

    val changeRate: Float,   // 환율 가격 변동률 = signedChangeRate
    val changePrice: Double,  // 환율 가격 변동가 = signedChangePrice
    val changeStatus: String,         // 오늘의 환율 등락 여부 : EVEN 보합, RISE 상승, FALL 하락 = change
    val unsignedChangeRate: Float,      // 오늘의 환율 변동률 절대값 = changeRate
    val unsignedChangePrice: Double,    // 오늘의 환율 변동가 절대값 = changePrice
    var createdAt: Long?      // 데이터 생성 일시
)

 

 - fiat rate info 정보 저장용

// vim FiatRateInfo.kt

package com.dev.kopring00.external.fiat.entities

import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document

@Document(collection = "fiatRateInfo")
data class FiatRateInfo(
    @Id
    val id: String?, // = fullCode
    val fullCode: String?,   // 통화 화폐 단위 포함 전체 코드
    val currencyUnit: Int?,  // 통화 화폐 단위 : 1 or 100
    val code: String,   // 환율 코드 : KRWJPY, USDKRW
    val currencyCode: String,   // 통화 코드 : JPY
    val currencyName: String?,  // 통화 이름 : 엔
    val paymentCurrencyCode: String,    // 지급 화폐 통화 코드 : KRW
    val paymentCurrencyName: String?,  // 지급 화폐 통화 이름 : 원
    val country: String?,   // 국가명 : 일본
    val searchKeyword: String?,  // 환율의 이름 : 일본 (JPY100/KRW)
    val fiatId: Short,
    val timestamp: Long,     // 정보 기준 일시
    val updatedAt: Long     // 데이터 업데이트된 시점 일시
)

 

 - fiat code 관련 정보 가공 및 추출용

// vim FiatCodeData.kt

package com.dev.kopring00.external.fiat.entities

data class FiatCodeData(
    val code: String,
    val fullCode: String?,
    val currencyUnit: Int?
)

 

 - exchange 관련 정보 가공 및 추출용

// vim ExchangeCashData.kt

package com.dev.kopring00.external.fiat.entities

data class ExchangeCashData(
    val buyingPrice: Double?,
    val buyingFeePrice: Double?,
    val buyingFeeRate: Float?,
    val sellingPrice: Double?,
    val sellingFeePrice: Double?,
    val sellingFeeRate: Float?,
)
// vim ExchangeTtData.kt

package com.dev.kopring00.external.fiat.entities

data class ExchangeTtData(
    val buyingPrice: Double?,
    val buyingFeePrice: Double?,
    val buyingFeeRate: Float?,
    val sellingPrice: Double?,
    val sellingFeePrice: Double?,
    val sellingFeeRate: Float?
)

 

 

 

 

 

8. Repository 소스코드 작성

 

// vim DunamuRateDataRepository.kt

package com.dev.kopring00.external.fiat.repositories

import com.dev.kopring00.external.fiat.entities.DunamuRateData
import org.springframework.data.mongodb.repository.MongoRepository

interface DunamuRateDataRepository: MongoRepository<DunamuRateData, String> {
    fun findFirstByOrderByTimestampDesc(): DunamuRateData?
}

 

// vim FiatRateInfoRepository.kt

package com.dev.kopring00.external.fiat.repositories

import com.dev.kopring00.external.fiat.entities.FiatRateInfo
import org.springframework.data.mongodb.repository.MongoRepository

interface FiatRateInfoRepository: MongoRepository<FiatRateInfo, String>

 

 

 

 

 

9. Service 소스코드 작성

 

// vim DunamuRateDataService.kt

package com.dev.kopring00.external.fiat.services

import com.dev.kopring00.base.entities.IsNeedUpdateData
import com.dev.kopring00.external.fiat.entities.*
import com.dev.kopring00.external.fiat.repositories.DunamuRateDataRepository
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import java.time.*
import java.time.format.DateTimeFormatter

@Service
class DunamuRateDataService(
    private val rateDataRepository: DunamuRateDataRepository
) {
    fun getRateDataFromApi(apiUrl: String): List<DunamuRateData_raw> {
        val restTemplate = RestTemplate()
        val response = restTemplate.getForEntity(apiUrl, Array<DunamuRateData_raw>::class.java)
        val rateDataArray = response.body ?: emptyArray()
        return rateDataArray.toList()
    }

    fun saveKrwRateData(dunamuRateDataRaw: DunamuRateData_raw) {
        /* ===== 정보 기준 일시 정보 timestamp로 변환 ===== */
        val datetimeKST: String = dunamuRateDataRaw.date + "T" + dunamuRateDataRaw.time    // 정보 기준 일시
        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
        val datetime: LocalDateTime = LocalDateTime.parse(datetimeKST, formatter)
        val zonedDatetime = ZonedDateTime.of(datetime, ZoneId.of("Asia/Seoul"))
        val timestamp = zonedDatetime.toEpochSecond()

        /* ===== 현금, tt 환전 정보 가공 및 생성 ===== */
        val exchangeCashData = extractExchangeCashData(dunamuRateDataRaw)
        val exchangeTtData = extractExchangeTtData(dunamuRateDataRaw)

        /* ===== 오늘 환율 변동 수치 정보 ===== */
        val changeRate: Float = (dunamuRateDataRaw.signedChangeRate * 100).toFloat()
        val changePrice: Double = dunamuRateDataRaw.signedChangePrice
        // 절대값
        val unsignedChangeRate: Float = (dunamuRateDataRaw.changeRate * 100).toFloat()
        val unsignedChangePrice: Double = dunamuRateDataRaw.changePrice

        // fullCode 임시 제작
        val currencyUnit = dunamuRateDataRaw.currencyUnit
        val code = dunamuRateDataRaw.code.split(".")[1]
        val fullCode: String?
        if (currencyUnit == 1) {
            fullCode = code
        } else {
            fullCode = currencyUnit.toString() + code
        }

        val dunamuRateData = DunamuRateData(
            fullCode = fullCode,
            timestamp = timestamp,    // 정보의 기준 일시
            basePrice = dunamuRateDataRaw.basePrice,   // 환율 기준 가격
            openingPrice = dunamuRateDataRaw.openingPrice, // 오늘의 환율 시가
            highPrice = dunamuRateDataRaw.highPrice,   // 오늘의 환율 고가
            lowPrice = dunamuRateDataRaw.lowPrice, // 오늘의 환율 저가
            /* ===== 현금 ===== */
            cashBuyingPrice = exchangeCashData.buyingPrice,  // 현금으로 구매시 적용 환율 가격 Buy
            cashSellingPrice = exchangeCashData.sellingPrice,    // 현금으로 판매시 적용 환율 가격 Sell
            cashBuyingFeeRate = exchangeCashData.buyingFeeRate,
            cashBuyingFeePrice = exchangeCashData.buyingFeePrice,
            cashSellingFeeRate = exchangeCashData.sellingFeeRate,
            cashSellingFeePrice = exchangeCashData.sellingFeePrice,
            /* ===== 전신환 ===== */
            ttBuyingPrice = exchangeTtData.buyingPrice,  // 전신환 구매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Buy
            ttSellingPrice = exchangeTtData.sellingPrice,    // 전신환 판매시 적용 환율 가격 (전신환 기관 간의 환전시 적용) Sell
            ttBuyingFeeRate = exchangeTtData.buyingFeeRate,
            ttBuyingFeePrice = exchangeTtData.buyingFeePrice,
            ttSellingFeeRate = exchangeTtData.sellingFeeRate,
            ttSellingFeePrice = exchangeTtData.sellingFeePrice,

            usDollarRate = dunamuRateDataRaw.usDollarRate, // 미국 달러 기준 환율 : 100JPY = 0.6776USD
            high52wPrice = dunamuRateDataRaw.high52wPrice, // 최근 52주 최고가
            high52wDate = dunamuRateDataRaw.high52wDate,   // 최근 52주 최고가 발생 일자
            low52wPrice = dunamuRateDataRaw.low52wPrice,   // 최근 52주 최저가
            low52wDate = dunamuRateDataRaw.low52wDate, // 최근 52주 최저가 발생 일자
            provider = dunamuRateDataRaw.provider, // 정보 출처 : 하나은행 or null
            changeRate = changeRate,    // 환율 가격 변동률
            changePrice = changePrice,  // 환율 가격 변동가
            changeStatus = dunamuRateDataRaw.change,   // 오늘의 환율 등락 여부 : EVEN 보합, RISE 상승, FALL 하락 = change
            unsignedChangeRate = unsignedChangeRate,    // 오늘의 환율 변동가 절대값 = unsignedChangePrice
            unsignedChangePrice = unsignedChangePrice,  // 오늘의 환율 변동률 절대값
            createdAt = dunamuRateDataRaw.timestamp     // 데이터 제공된 external api의 당시 서버 시간
        )
        rateDataRepository.save(dunamuRateData)
    }

    /* ===== 현금 환전 정보 ===== */
    fun extractExchangeCashData(dunamuRateDataRaw: DunamuRateData_raw): ExchangeCashData {
        val basePrice = dunamuRateDataRaw.basePrice

        val buyingPrice = if (dunamuRateDataRaw.cashBuyingPrice != 0.00) dunamuRateDataRaw.cashBuyingPrice else null
        val buyingFeePrice = buyingPrice?.let { it - basePrice }
        val buyingFeeRate = buyingFeePrice?.let { (it / basePrice * 100).toFloat() }

        val sellingPrice = if (dunamuRateDataRaw.cashSellingPrice != 0.00) dunamuRateDataRaw.cashSellingPrice else null
        val sellingFeePrice = sellingPrice?.let { -(it - basePrice) }
        val sellingFeeRate = sellingFeePrice?.let { (it / basePrice * 100).toFloat() }

        return ExchangeCashData(
            buyingPrice, buyingFeePrice, buyingFeeRate,
            sellingPrice, sellingFeePrice, sellingFeeRate
        )
    }

    /* ===== tt 환전 정보 ===== */
    fun extractExchangeTtData(dunamuRateDataRaw: DunamuRateData_raw): ExchangeTtData {
        val basePrice = dunamuRateDataRaw.basePrice

        val buyingPrice = if (dunamuRateDataRaw.ttBuyingPrice != 0.00) dunamuRateDataRaw.ttBuyingPrice else null
        val buyingFeePrice = buyingPrice?.let { it - basePrice }
        val buyingFeeRate = buyingFeePrice?.let { (it / basePrice * 100).toFloat() }

        val sellingPrice = if (dunamuRateDataRaw.ttSellingPrice != 0.00) dunamuRateDataRaw.ttSellingPrice else null
        val sellingFeePrice = sellingPrice?.let { -(it - basePrice) }
        val sellingFeeRate = sellingFeePrice?.let { (it / basePrice * 100).toFloat() }

        return ExchangeTtData(
            buyingPrice, buyingFeePrice, buyingFeeRate,
            sellingPrice, sellingFeePrice, sellingFeeRate
        )
    }


    // 데이터 최신화 필요 여부 확인
    // 날짜, 시간까지만 일치 여부 확인 : "yyyy-MM-dd HH"
    fun isNeedUpdateBeforeCall(): IsNeedUpdateData {
        val currentInstant = Instant.now()
        val lastSavedDatetime: DunamuRateData? = rateDataRepository.findFirstByOrderByTimestampDesc()
        // 데이터가 있는 경우, 데이터 최신화 필요한지 체크
        if (lastSavedDatetime != null) {
            val instant = Instant.ofEpochSecond(lastSavedDatetime.timestamp)
            // 저장된 timestamp에서 1분이 지난 timestamp가 현재보다 과거이면 true, 현재보다 미래이면 아직 1분이 안지난거니까 false
            // ofMinutes를 2로 하면 수집된 데이터 기준 일시보다 최소 2분 이상은 지난 뒤에 update 시도를 함
            return IsNeedUpdateData(status = instant.plus(Duration.ofMinutes(2)).isBefore(currentInstant), data = lastSavedDatetime.timestamp.toString())
        }
        // 데이터가 없으면 즉시 update 실행
        return IsNeedUpdateData(status = true, data = null)
    }

    fun isNeedUpdateAfterCall(timestampSaved: Long, fullApiUrl: String): Boolean {
        /* ===== 정보 기준 일시 정보 timestamp로 변환 ===== */
        val newDataList: List<DunamuRateData_raw> = getRateDataFromApi(fullApiUrl)
        // 데이터가 있는 경우
        if (newDataList.isNotEmpty()) {
            /* ===== 정보 기준 일시 정보 timestamp로 변환 ===== */
            val datetimeKST: String = newDataList[0].date + "T" + newDataList[0].time    // 정보 기준 일시
            val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
            val datetime: LocalDateTime = LocalDateTime.parse(datetimeKST, formatter)
            val zonedDatetime = ZonedDateTime.of(datetime, ZoneId.of("Asia/Seoul"))
            val newTimestamp = zonedDatetime.toEpochSecond()
            // 저장된 데이터랑 신규 데이터가 다르면 업데이트 진행
            return newTimestamp != timestampSaved
        }
        // 신규 데이터가 없으면 불러온 데이터로 데이터를 업데이트 할 수 없으므로 미진행
        return false
    }

}

 

// vim FiatRateDataService.kt

package com.dev.kopring00.external.fiat.services

import com.dev.kopring00.external.fiat.entities.DunamuRateData_raw
import com.dev.kopring00.external.fiat.entities.FiatCodeData
import com.dev.kopring00.external.fiat.entities.FiatRateInfo
import com.dev.kopring00.external.fiat.repositories.FiatRateInfoRepository
import com.dev.kopring00.external.fiat.schedulers.DunamuRateDataScheduler
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

@Service
class FiatRateInfoService(
    private val fiatRateInfoRepository: FiatRateInfoRepository
) {
    fun saveRateDataFromData(dunamuRateDataRaw: DunamuRateData_raw) {

        /* ===== 정보 기준 일시 정보 timestamp로 변환 ===== */
        val datetimeKST: String = dunamuRateDataRaw.date + "T" + dunamuRateDataRaw.time    // 정보 기준 일시
        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
        val datetime: LocalDateTime = LocalDateTime.parse(datetimeKST, formatter)
        val zonedDatetime = ZonedDateTime.of(datetime, ZoneId.of("Asia/Seoul"))
        val timestamp = zonedDatetime.toEpochSecond()

        // 환율 코드 관련 주요 정보 추출 & 데이터 상태 정상 여부 확인
        val fiatExtractedCodeInfo = extractFiatCode(dunamuRateDataRaw)
        val currencyCode = fiatExtractedCodeInfo.code
        val fullCode = fiatExtractedCodeInfo.fullCode
        val currencyUnit = fiatExtractedCodeInfo.currencyUnit

        // 화폐 이름 단위 null인 값 예외처리
        val currencyName = makeCurrencyName(dunamuRateDataRaw.currencyName, currencyCode)

        // 국가명 null인 경우 예외처리
        val country = makeCountry(dunamuRateDataRaw.country, currencyCode)

        // 검색 키워드 생성
        val searchKeyword: String = buildString {
            append(dunamuRateDataRaw.name ?: country)
            append(" $fullCode $currencyCode $currencyName ${dunamuRateDataRaw.id}")
        }

        val fiatRateInfo = FiatRateInfo(
            id = fullCode,
            fullCode = fullCode,
            currencyUnit = currencyUnit,
            code = currencyCode,
            currencyCode = currencyCode,
            currencyName = currencyName,
            paymentCurrencyCode = "KRW",
            paymentCurrencyName = "원",
            country = country,
            searchKeyword = searchKeyword,
            fiatId = dunamuRateDataRaw.id, // 고유 식별값
            timestamp = timestamp,
            updatedAt = Instant.now().epochSecond
        )
        fiatRateInfoRepository.save(fiatRateInfo)
    }

    // 환율 코드 관련 주요 정보 추출 & 데이터 상태 정상 여부 확인
    fun extractFiatCode(dunamuRateDataRaw: DunamuRateData_raw): FiatCodeData {
        val currencyCode = dunamuRateDataRaw.currencyCode
        val code: String
        val fullCode: String?
        val currencyUnit: Int?
        // fiat currency 정보 예상되는 형태 값인지 확인
        if ("FRX.KRW$currencyCode" == dunamuRateDataRaw.code) {
            code = "$currencyCode" + "KRW"
            currencyUnit = dunamuRateDataRaw.currencyUnit
            fullCode = if (currencyUnit == 1) code else "${currencyUnit}$code"
        } else {
            // KRWUSD처럼 깔끔한 형태의 코드 이외의 코드가 출현할 경우 감지
            val logger: Logger = LoggerFactory.getLogger(DunamuRateDataScheduler::class.java)
            logger.error("[ ExternalApi:Dunamu ] Didn't expect code: ${dunamuRateDataRaw.code}")
            code = ""
            currencyUnit = 1
            fullCode = ""
        }
        return FiatCodeData(code, fullCode, currencyUnit)
    }

    fun makeCurrencyName(currencyName: String?, currencyCode: String) : String {
        return currencyName ?: when (currencyCode) {
            "CNH" -> "위안"
            "CLP" -> "페소"
            "COP" -> "페소"
            "OMR" -> "리알"
            "RON" -> "레위"
            "DZD" -> "디나르"
            "ETB" -> "비르"
            "FJD" -> "달러"
            "KES" -> "실링"
            "KHR" -> "리얄"
            "LKR" -> "루피"
            "LYD" -> "디나르"
            "MMK" -> "키얏"
            "MOP" -> "파타카"
            "NPR" -> "루피"
            "TZS" -> "실링"
            "UZS" -> "솜"
            else -> currencyCode
        }
    }

    fun makeCountry(country: String?, currencyCode: String): String {
        return country ?: when (currencyCode) {
            "CNH" -> "홍콩"
            "CLP" -> "칠레"
            "COP" -> "콜롬비아"
            "OMR" -> "오만"
            "RON" -> "루마니아"
            "DZD" -> "알제리"
            "ETB" -> "에티오피아"
            "FJD" -> "피지"
            "KES" -> "케냐"
            "KHR" -> "캄보디아"
            "LKR" -> "스리랑카"
            "LYD" -> "리비아"
            "MMK" -> "미얀마"
            "MOP" -> "마카오"
            "NPR" -> "네팔"
            "TZS" -> "탄자니아"
            "UZS" -> "우즈베키스탄"
            else -> currencyCode
        }
    }
}

 

 

 

 

 

10. Schedulers 소스코드 작성

 

// vim DunamuRateDataScheduler.kt

package com.dev.kopring00.external.fiat.schedulers

import com.dev.kopring00.external.fiat.services.DunamuRateDataService
import com.dev.kopring00.external.fiat.services.FiatRateInfoService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class DunamuRateDataScheduler @Autowired constructor(
    private val dunamuRateDataService: DunamuRateDataService,
    private val fiatRateInfoService: FiatRateInfoService
) {

    @Value("\${api.url.dunamuRate}")
    private lateinit var apiUrl: String

    val logger: Logger = LoggerFactory.getLogger(DunamuRateDataScheduler::class.java)

    fun updateRateDataKRW() {
        val fiatList = listOf("USD", "EUR", "JPY", "GBP", "CHF", "CAD", "AUD", "CNY", "HKD", "SEK", "NZD", "SGD", "NOK", "MXN", "INR", "RUB", "ZAR", "TRY", "BRL", "AED", "BHD", "BND", "CNH", "CZK", "DKK", "IDR", "ILS", "MYR", "QAR", "SAR", "THB", "TWD", "CLP", "COP", "EGP", "HUF", "KWD", "OMR", "PHP", "PLN", "PKR", "RON", "BDT", "DZD", "ETB", "FJD", "JOD", "KES", "KHR", "KZT", "LKR", "LYD", "MMK", "MNT", "MOP", "NPR", "TZS", "UZS", "VND")
        for (fiat in fiatList) {
            val fullApiUrl = apiUrl + "KRW" + fiat
            val rateDataList = dunamuRateDataService.getRateDataFromApi(fullApiUrl)
            if (rateDataList.isNotEmpty()) {
                val dunamuRateData = rateDataList[0]
                // 환율 info 최신화
                fiatRateInfoService.saveRateDataFromData(dunamuRateData)
                // 환율 data 추가 저장
                dunamuRateDataService.saveKrwRateData(dunamuRateData)
//                logger.info("[ External Api ] Success save Data KRW$fiat: OpenExchangeRate") // 화폐별로 환율 정보 수집 정상 확인 체크용
            } else {
                logger.error("[ ExternalApi:Dunamu ] Response is Empty on KRW$fiat")
            }
        }
    }

    // 60초=1분마다 실행
    @Scheduled(fixedRate = 60000)
    fun scheduledUpdate() {
        /* ########## 업데이트 필요 여부 1차 확인 ########## */
        logger.info("[ ExternalApi:Dunamu ] is Need Update Before Call Check worked")
        val status = dunamuRateDataService.isNeedUpdateBeforeCall().status
        val timestampSaved = dunamuRateDataService.isNeedUpdateBeforeCall().data
        if (status) {
            /* ########## 업데이트 필요 여부 2차 확인 ########## */
            logger.info("[ ExternalApi:Dunamu ] is Need Update After Call Check worked")
            if (timestampSaved != null) {
                if (dunamuRateDataService.isNeedUpdateAfterCall(timestampSaved.toLong(), apiUrl+"KRWUSD") == true) {
                    // 신규 데이터가 기존 데이터랑 다르니 업데이트 진행
                    logger.info("[ ExternalApi:Dunamu ] is Need Update After Call Confirmed timestamp")
                    updateRateDataKRW()
                } else {
                    logger.info("[ ExternalApi:Dunamu ] is Need Update After Call Standby")
                }
            } else {
                // 신규 데이터가 있고 기존 데이터가 없으니 이 또한 다른 것이므로 업데이트 진행
                logger.info("[ ExternalApi:Dunamu ] is Need Update After Call Confirmed timestamp")
                updateRateDataKRW()
            }
        }
    }

}

 

 

 

 

 

11. 결과 예제

 

 

 

 

 

 

 

 

+ Recent posts