From 59c8ff4ebb75070f6be6ef4d1f7dad6896dc3fb3 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 19 Feb 2026 16:02:29 +0900 Subject: [PATCH] ... --- src/main/kotlin/model/StockModels.kt | 2 + src/main/kotlin/network/KisTradeService.kt | 126 ++++++++++++--------- 2 files changed, 76 insertions(+), 52 deletions(-) diff --git a/src/main/kotlin/model/StockModels.kt b/src/main/kotlin/model/StockModels.kt index eb14495..b404610 100644 --- a/src/main/kotlin/model/StockModels.kt +++ b/src/main/kotlin/model/StockModels.kt @@ -8,6 +8,8 @@ import kotlinx.serialization.Serializable data class StockBalanceResponse( val rt_cd: String = "", val msg1: String = "", + val ctx_area_fk100: String = "", + val ctx_area_nk100: String = "", val output1: List = emptyList(), val output2: List = emptyList() ) diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index fb6a71b..0eff26e 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -505,65 +505,87 @@ object KisTradeService { // --- 내부 Raw 호출용 (통합 잔고에서 사용) --- private suspend fun fetchDomesticRawBalance(): Result { - val config = KisSession.config - val baseUrl = prodUrl - val trId = "TTTC8434R" + val config = KisSession.config + val baseUrl = prodUrl + val trId = "TTTC8434R" - val allHoldings = mutableListOf() - var totalBalance: StockBalanceResponse? = null + val allHoldings = mutableListOf() + var totalBalance: StockBalanceResponse? = null - // 연속 조회를 위한 변수 - var ctxAreaFk = "" - var ctxAreaNk = "" - var trCont = "N" // 'N': 최초 조회, 'F': 다음 조회, 'M': 연속 조회 + var ctxAreaFk = "" + var ctxAreaNk = "" + var trCont = "" + var pageCount = 1 + var pureAccount = config.accountNo.replace("-", "").trim() + if (pureAccount.length == 8) pureAccount += "01" - try { - do { - val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") { - header("authorization", "Bearer ${config.tradeToken}") - header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey) - header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey) - header("tr_id", trId) - header("tr_cont", trCont) // 연속 조회 키 설정 + val cano = pureAccount.take(8) + val acntPrdtCd = pureAccount.takeLast(2) + println("🚀 [잔고조회 시작] 계좌: ${config.realAccountNo}") - val pureAccount = config.realAccountNo.replace("-", "").trim() - parameter("CANO", pureAccount.take(8)) - parameter("ACNT_PRDT_CD", pureAccount.takeLast(2)) - parameter("AFHR_FLPR_YN", "N") - parameter("OFL_YN", "N") - parameter("INQR_DVSN", "0") - parameter("UNPR_DVSN", "01") - parameter("FUND_STTL_ICLD_YN", "N") - parameter("FNCG_AMT_AUTO_RDPT_YN", "N") - parameter("PRCS_DVSN", "00") - // 연속 조회 파라미터 전달 - parameter("CTX_AREA_FK100", ctxAreaFk) - parameter("CTX_AREA_NK100", ctxAreaNk) - } + try { + do { + println("📡 [Step $pageCount] 요청 전송 중... (tr_cont: $trCont)") + val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") { + header("authorization", "Bearer ${config.tradeToken}") + header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey) + header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey) + header("tr_id", trId) + header("tr_cont", trCont) - val body = response.body() - - // 데이터 합치기 - allHoldings.addAll(body.output1) - if (totalBalance == null) totalBalance = body - - // 헤더에서 다음 조회를 위한 키값 추출 - trCont = response.headers["tr_cont"] ?: "D" // 'D' 또는 'E'는 끝을 의미 - ctxAreaFk = response.headers["ctx_area_fk100"] ?: "" - ctxAreaNk = response.headers["ctx_area_nk100"] ?: "" - delay(250) - } while (trCont == "F" || trCont == "M") // 연속 데이터가 있는 동안 반복 - // 모든 데이터를 합친 최종 객체 반환 - return if (totalBalance != null) { - Result.success(totalBalance.copy(output1 = allHoldings)) - } else { - println(totalBalance.toString()) - Result.failure(Exception("No data found")) + parameter("CANO", cano) + parameter("ACNT_PRDT_CD", acntPrdtCd) + parameter("AFHR_FLPR_YN", "N") + parameter("OFL_YN", "N") + parameter("INQR_DVSN", "0") + parameter("UNPR_DVSN", "01") + parameter("FUND_STTL_ICLD_YN", "N") + parameter("FNCG_AMT_AUTO_RDPT_YN", "N") + parameter("PRCS_DVSN", "00") + parameter("CTX_AREA_FK100", ctxAreaFk) + parameter("CTX_AREA_NK100", ctxAreaNk) } - } catch (e: Exception) { - e.printStackTrace() - return Result.failure(e) + + if (!response.status.isSuccess()) { + println("❌ [Step $pageCount] HTTP 에러 발생: ${response.status}") + return Result.failure(Exception("HTTP Error: ${response.status}")) + } + + val body = response.body() + println("✅ [Step $pageCount] 수신 완료 - 종목 수: ${body.output1.size}") + + allHoldings.addAll(body.output1) + if (totalBalance == null) totalBalance = body + + // 다음 페이지를 위한 헤더 정보 추출 + trCont = response.headers["tr_cont"] ?: "D" + ctxAreaFk = body.ctx_area_fk100 ?: "" + ctxAreaNk = body.ctx_area_nk100 ?: "" + + println("📝 [Header Check] tr_cont: $trCont, ctx_area_nk100: $ctxAreaNk") + + if ( trCont == "M") { + pageCount++ + trCont = "N" + println("⏳ [연속 조회] 250ms 대기 후 다음 페이지 요청...") + delay(250) // API 과부하 방지 + } + + } while (trCont == "N") + + println("🎊 [잔고조회 종료] 총 수집 종목: ${allHoldings.size}") + + return if (totalBalance != null) { + Result.success(totalBalance.copy(output1 = allHoldings)) + } else { + Result.failure(Exception("응답 바디가 비어있습니다.")) } + + } catch (e: Exception) { + println("💥 [Fatal Error] 잔고 조회 중 예외 발생: ${e.message}") + e.printStackTrace() + return Result.failure(e) + } } private suspend fun fetchOverseasRawBalance(): Result {