1. 이전 포스팅
https://growingsaja.tistory.com/986
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. 결과 예제