atrade/src/main/kotlin/network/KisAuthService.kt
2026-03-26 14:42:39 +09:00

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