[Kotlin][SpringBoot3] Kopring 서버 기본 실습 17 - 외부 api를 활용해 Scheduler로 정보 수집 실습 4 : dunamu api로 주요 국가 환율 정보 수집, 가공, 저장 기능 구현 (USD)
1. 이전 포스팅
https://growingsaja.tistory.com/985
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. 데이터 정상 수집 작동 확인