Development/Spring Boot3 (Kotlin)

[Kotlin][SpringBoot3] Kopring 서버 기본 실습 17 - 외부 api를 활용해 Scheduler로 정보 수집 실습 4 : dunamu api로 주요 국가 환율 정보 수집, 가공, 저장 기능 구현 (USD)

Tradgineer 2023. 10. 13. 13:18

 

1. 이전 포스팅

 

https://growingsaja.tistory.com/985

 

[Kotlin][SpringBoot3] Kopring 서버 기본 실습 16 - 외부 api를 활용해 Scheduler로 정보 수집 실습 3 : dunamu api

1. 이전 포스팅 https://growingsaja.tistory.com/986 [Kotlin][SpringBoot3] Kopring 서버 기본 실습 15 - 외부 api를 활용해 Scheduler로 정보 수집 실습 2 : open excha 1. 이전 포스팅 https://growingsaja.tistory.com/981 [Kotlin][Spring

growingsaja.tistory.com

 

 

 

 

 

2. 목표

 

 - Dunamu에서의 USD 기준 환율 정보 수집 기능 -> DunamuRateData

 - 주요 국가 USD 기준 환율 정보 최신화 -> 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"

 

 

 

 

 

4. api response 데이터 예제 살펴보기

 

 - 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
    }
]

 

 - 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. 프로젝트 구조 및 주요 소스코드

 

 

 

 

 

 

7. 수집해 저장할 정보 설정

 

 KRW와 달리 일부 데이터만 저장합니다.

 

// vim DunamuUsdRateData.kt

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

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

@Document(collection = "dunamuUsdRate")
data class DunamuUsdRateData(
    @Indexed
    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 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?      // 데이터 생성 일시
)

 

// vim DunamuUsdRateRepository.kt

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

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

interface DunamuUsdRateRepository: MongoRepository<DunamuUsdRateData, String> {
}

 

// 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      // 오늘의 환율 변동률 절대값
)

 

 

 

 

 

8. 서비스 및 스케줄러

 

// vim DunamuRateService.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.DunamuKrwRateRepository
import com.dev.kopring00.external.fiat.repositories.DunamuUsdRateRepository
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import java.time.*
import java.time.format.DateTimeFormatter

@Service
class DunamuRateService(
    private val krwRateRepository: DunamuKrwRateRepository,
    private val usdRateRepository: DunamuUsdRateRepository
) {
    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 saveUsdRateData(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 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 code = "USD" + dunamuRateDataRaw.currencyCode

        val dunamuRateData = DunamuUsdRateData(
            fullCode = code,
            timestamp = timestamp,    // 정보의 기준 일시
            basePrice = dunamuRateDataRaw.basePrice,   // 환율 기준 가격
            openingPrice = dunamuRateDataRaw.openingPrice, // 오늘의 환율 시가
            highPrice = dunamuRateDataRaw.highPrice,   // 오늘의 환율 고가
            lowPrice = dunamuRateDataRaw.lowPrice, // 오늘의 환율 저가

            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의 당시 서버 시간
        )
        usdRateRepository.save(dunamuRateData)
    }

    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.currencyCode + "KRW"
        val fullCode: String
        fullCode = if (currencyUnit == 1) {
            code
        } else {
            currencyUnit.toString() + code
        }

        val dunamuKrwRateData = DunamuKrwRateData(
            id = null,
            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의 당시 서버 시간
        )
        krwRateRepository.save(dunamuKrwRateData)
    }

    /* ===== KRW 관련 현금 환전 정보 ===== */
    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
        )
    }

    /* ===== KRW 관련 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 lastSavedKrwRateDatetime: DunamuKrwRateData? = krwRateRepository.findFirstByOrderByIdDesc()
        // 데이터가 있는 경우, 데이터 최신화 필요한지 체크
        if (lastSavedKrwRateDatetime != null) {
            val instant = Instant.ofEpochSecond(lastSavedKrwRateDatetime.timestamp)
            // 저장된 timestamp에서 1분이 지난 timestamp가 현재보다 과거이면 true, 현재보다 미래이면 아직 1분이 안지난거니까 false
            // ofMinutes를 2로 하면 수집된 데이터 기준 일시보다 최소 2분 이상은 지난 뒤에 update 시도를 함
            return IsNeedUpdateData(status = instant.plus(Duration.ofMinutes(2)).isBefore(currentInstant), data = lastSavedKrwRateDatetime.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_live
import com.dev.kopring00.external.fiat.repositories.FiatRateInfoRepository
import com.dev.kopring00.external.fiat.schedulers.DunamuRateScheduler
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
) {
    val logger: Logger = LoggerFactory.getLogger(DunamuRateScheduler::class.java)
    fun saveRateInfo(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: FiatCodeData?
        if (10 <= dunamuRateDataRaw.code.length) {
            if (dunamuRateDataRaw.code.substring(4, 7) == "KRW") {
                // KRW
                fiatExtractedCodeInfo = extractKrwRateCode(dunamuRateDataRaw)
            } else if (dunamuRateDataRaw.code.substring(7, 10) == "USD") {
                // USD
                fiatExtractedCodeInfo = extractUsdRateCode(dunamuRateDataRaw)
            } else {
                // 이외
                logger.error("[ ExternalApi:Dunamu ] dunamuRateDataRaw data is not in (USD, KRW) -> code : ${dunamuRateDataRaw.code}, substring : ${dunamuRateDataRaw.code.substring(4, 7)}")
                fiatExtractedCodeInfo = extractUsdRateCode(dunamuRateDataRaw)
            }
        } else {
            fiatExtractedCodeInfo = null
            logger.error("[ ExternalApi:Dunamu ] dunamuRateDataRaw.code length has to be >= 10")
        }
        val currencyCode = fiatExtractedCodeInfo?.currencyCode
        val fullCode = fiatExtractedCodeInfo?.fullCode

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

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

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

        val fiatRateInfoLive = fiatExtractedCodeInfo?.code?.let {
            FiatRateInfo_live(
                id = fullCode,
                fullCode = fullCode,
                currencyUnit = fiatExtractedCodeInfo?.currencyUnit,
                code = it,
                currencyCode = currencyCode,
                currencyName = currencyName,
                paymentCurrencyCode = "KRW",
                paymentCurrencyName = "원",
                country = country,
                searchKeyword = searchKeyword,
                fiatId = dunamuRateDataRaw.id, // 고유 식별값
                timestamp = timestamp,
                updatedAt = Instant.now().epochSecond
            )
        }
        if (fiatRateInfoLive != null) {
            fiatRateInfoRepository.save(fiatRateInfoLive)
        } else {
            logger.error("[ ExternalApi:Dunamu ] dunamuRateData fiatRateInfo is null")
        }
    }

    // KRW 기준 환율 코드 관련 주요 정보 추출 & 데이터 상태 정상 여부 확인
    fun extractKrwRateCode(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처럼 깔끔한 형태의 코드 이외의 코드가 출현할 경우 감지
            logger.error("[ ExternalApi:Dunamu ] KRW Didn't expect -> dunamu code: ${dunamuRateDataRaw.code}, code: ${currencyCode}")
            code = ""
            currencyUnit = 1
            fullCode = ""
        }
        return FiatCodeData(code, currencyCode, fullCode, currencyUnit)
    }

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

 

// vim DunamuRateScheduler.kt

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

import com.dev.kopring00.external.fiat.services.DunamuRateService
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 DunamuRateScheduler @Autowired constructor(
    private val dunamuRateService: DunamuRateService,
    private val fiatRateInfoService: FiatRateInfoService
) {

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

    val logger: Logger = LoggerFactory.getLogger(DunamuRateScheduler::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 = dunamuRateService.getRateDataFromApi(fullApiUrl)
            if (rateDataList.isNotEmpty()) {
                val dunamuRateData = rateDataList[0]
                // 환율 info 최신화
                fiatRateInfoService.saveRateInfo(dunamuRateData)
                // 환율 data 추가 저장
                dunamuRateService.saveKrwRateData(dunamuRateData)
//                logger.info("[ External Api ] Success save Data KRW$fiat: OpenExchangeRate") // 화폐별로 환율 정보 수집 정상 확인 체크용
            } else {
                logger.error("[ ExternalApi:Dunamu ] Response is Empty on KRW$fiat")
            }
        }
    }

    fun updateRateDataUSD() {
        val fiatList = listOf("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 + fiat + "USD"
            val rateDataList = dunamuRateService.getRateDataFromApi(fullApiUrl)
            if (rateDataList.isNotEmpty()) {
                val dunamuRateData = rateDataList[0]
                // 환율 info 최신화
                fiatRateInfoService.saveRateInfo(dunamuRateData)
                // 환율 data 추가 저장
                dunamuRateService.saveUsdRateData(dunamuRateData)
//                logger.info("[ External Api ] Success save Data KRW$fiat: OpenExchangeRate") // 화폐별로 환율 정보 수집 정상 확인 체크용
            } else {
                logger.error("[ ExternalApi:Dunamu ] Response is Empty on ${fiat}USD")
            }
        }
    }

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

 

 

 

 

 

9. 데이터 정상 수집 작동 확인