91 lines
3.5 KiB
Kotlin
91 lines
3.5 KiB
Kotlin
package network
|
|
|
|
import io.ktor.client.*
|
|
import io.ktor.client.call.*
|
|
import io.ktor.client.engine.cio.CIO
|
|
import io.ktor.client.plugins.contentnegotiation.*
|
|
import io.ktor.client.plugins.logging.DEFAULT
|
|
import io.ktor.client.plugins.logging.LogLevel
|
|
import io.ktor.client.plugins.logging.Logger
|
|
import io.ktor.client.plugins.logging.Logging
|
|
import io.ktor.client.request.*
|
|
import io.ktor.http.*
|
|
import io.ktor.serialization.kotlinx.json.*
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.coroutineScope
|
|
import model.KisSession
|
|
import model.TokenRequest
|
|
import model.TokenResponse
|
|
import service.AutoTradingManager
|
|
import java.time.LocalDateTime
|
|
|
|
object KisAuthService {
|
|
private val client = HttpClient(CIO) {
|
|
install(ContentNegotiation) {
|
|
json(Json {
|
|
ignoreUnknownKeys = true
|
|
encodeDefaults = true // 기본값이 포함된 요청 바디를 정확히 전송하기 위해 필요
|
|
})
|
|
}
|
|
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
|
install(Logging) {
|
|
logger = Logger.DEFAULT
|
|
level = LogLevel.NONE // 상세한 디버깅을 위해 ALL로 변경
|
|
}
|
|
}
|
|
|
|
private fun getBaseUrl(isSimulation: Boolean) =
|
|
if (isSimulation) "https://openapivts.koreainvestment.com:29443"
|
|
else "https://openapi.koreainvestment.com:9443"
|
|
|
|
/**
|
|
* 실전(시세용)과 매매(모의/실전 선택) 토큰을 모두 갱신합니다.
|
|
*/
|
|
suspend fun refreshAllTokens(): Boolean = coroutineScope {
|
|
val config = KisSession.config
|
|
// 1. 실전 시세용 토큰 발급 (Market Token)
|
|
val marketTokenJob = async { fetchAccessToken(config.realAppKey, config.realSecretKey, false) }
|
|
|
|
// 2. 매매용 토큰 발급 (Trade Token - 설정에 따라 VTS 또는 Real 사용)
|
|
val tradeTokenJob = async {
|
|
if (config.isSimulation) fetchAccessToken(config.vtsAppKey, config.vtsSecretKey, true)
|
|
else marketTokenJob.await() // 실전 매매면 시세용 토큰과 동일함
|
|
}
|
|
|
|
val mResult = marketTokenJob.await()
|
|
val tResult = tradeTokenJob.await()
|
|
|
|
if (mResult.isSuccess && tResult.isSuccess) {
|
|
val mData = mResult.getOrThrow()
|
|
val tData = tResult.getOrThrow()
|
|
|
|
// KisSession 업데이트
|
|
KisSession.config = KisSession.config.copy(
|
|
marketToken = mData.access_token,
|
|
marketTokenExpiredAt = LocalDateTime.now().plusSeconds(mData.expires_in),
|
|
tradeToken = tData.access_token,
|
|
tradeTokenExpiredAt = LocalDateTime.now().plusSeconds(tData.expires_in),
|
|
)
|
|
AutoTradingManager.tradeToken = true
|
|
true
|
|
} else {
|
|
AutoTradingManager.tradeToken = false
|
|
false
|
|
}
|
|
}
|
|
|
|
private suspend fun fetchAccessToken(appKey: String, secretKey: String, isSim: Boolean): Result<TokenResponse> {
|
|
return try {
|
|
val response = client.post("${getBaseUrl(isSim)}/oauth2/tokenP") {
|
|
contentType(ContentType.Application.Json)
|
|
setBody(TokenRequest("client_credentials", appKey, secretKey))
|
|
}
|
|
if (response.status == HttpStatusCode.OK) Result.success(response.body())
|
|
else Result.failure(Exception("인증 실패: ${response.status}"))
|
|
} catch (e: Exception) {
|
|
println("fetchAccessToken ${e.message}")
|
|
Result.failure(e)
|
|
}
|
|
}
|
|
} |