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 java.time.LocalDateTime class 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), ) true } else { false } } private suspend fun fetchAccessToken(appKey: String, secretKey: String, isSim: Boolean): Result { 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) { Result.failure(e) } } }