This commit is contained in:
2026-02-09 15:32:31 +09:00
parent aa1f3817bb
commit ce976a895d
7 changed files with 390 additions and 183 deletions
+57 -80
View File
@@ -10,6 +10,8 @@ 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.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
@@ -112,98 +114,73 @@ object KisTradeService {
*/
private suspend fun fetchDomesticRanking(type: RankingType): Result<List<RankingStock>> {
val config = KisSession.config
val jsonParser = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
}
return try {
val response = client.get("$prodUrl${type.path}") {
val response = client.get("${prodUrl}${type.path}") {
header("authorization", "Bearer ${config.marketToken}")
header("appkey", config.realAppKey)
header("appsecret", config.realSecretKey)
header("tr_id", type.trId)
header("custtype", "P")
header("Accept", "application/json")
parameter("FID_COND_MRKT_DIV_CODE", "J")
parameter("FID_COND_SCR_DIV_CODE", type.scrNo)
parameter("FID_INPUT_ISCD", "0000") // 전체 시장
parameter("FID_DIV_CLS_CODE", "0") // 전체
// [Step 1] 파라미터 기본값 재설정 (성공했던 원본 코드 기준)
val params = mutableMapOf(
"FID_COND_MRKT_DIV_CODE" to "J",
"FID_COND_SCR_DIV_CODE" to type.scrNo,
"FID_INPUT_ISCD" to "0000",
"FID_DIV_CLS_CODE" to "0",
"FID_BLNG_CLS_CODE" to "0",
"FID_RANK_SORT_CLS_CODE" to "0",
// "FID_MK_OP_CLS_CODE" 삭제! (이게 있으면 RISE/FALL이 깨짐)
"FID_PRC_CLS_CODE" to "0",
"FID_INPUT_CNT_1" to "0", // 1이 아니라 0이어야 함
"FID_INPUT_PRICE_1" to "",
"FID_INPUT_PRICE_2" to "",
"FID_VOL_CNT" to "",
"FID_RSFL_RATE1" to "",
"FID_RSFL_RATE2" to "", // 누락되었던 파라미터 추가
"FID_INPUT_DATE_1" to "",
"FID_INPUT_DATE_2" to "",
"FID_APLY_RANG_VOL" to "",
"FID_APLY_RANG_PRC_1" to "",
"FID_APLY_RANG_PRC_2" to "",
"FID_PERIOD_DIV_CODE" to "",
"FID_SELECT_DIV_CODE" to "",
"FID_INPUT_OPTION_1" to "",
"FID_INPUT_OPTION_2" to "",
"FID_TRGT_CLS_CODE" to "11111111",
"FID_TRGT_EXLS_CLS_CODE" to "000000"
)
parameter("FID_ETC_CLS_CODE", "0")
parameter("FID_PRC_CLS_CODE", "0")
when(type) {
RankingType.VALUE -> {
parameter("FID_BLNG_CLS_CODE", type.sortCode)
}
RankingType.VOLUME -> {
parameter("FID_BLNG_CLS_CODE",type.sortCode)
}
RankingType.FALL -> {
parameter("FID_RANK_SORT_CLS_CODE", type.sortCode)
// [Step 2] Enum 특화 설정 덮어쓰기
params.putAll(type.extraParams)
}
RankingType.RISE -> {
parameter("FID_RANK_SORT_CLS_CODE", type.sortCode)
}
// RankingType.AFTER -> {
// parameter("FID_MKOP_CLS_CODE", type.sortCode)
// }
// RankingType.BEFORE -> {
// parameter("FID_MKOP_CLS_CODE", type.sortCode)
// }
// RankingType.FOREIGNER_BUY, RankingType.INSTITUTION_BUY -> {
// parameter("FID_BLNG_CLS_CODE", type.sortCode) // 순매수용
// parameter("FID_INPUT_CNT_1", "5") // 5일 누적 등 조정 가능
// }
// RankingType.PER_RANK, RankingType.PBR_RANK -> {
// parameter("FID_FINCL_CLS_CODE", type.sortCode) // 재무비율용
// }
RankingType.AFTER_HOURS_VOLUME -> {
parameter("FID_TIME_OUT_CLS_CODE", "1") // 시간외 구분
}
RankingType.EXPECTED_RISE -> parameter("FID_MK_OP_CLS_CODE", type.sortCode)
// 체결강도/호가 등은 FID_EXEC_CLS_CODE 추가
else -> parameter("FID_BLNG_CLS_CODE", "0") // 기본
// else -> {
//
// }
}
parameter("FID_PBMN", "")
parameter("FID_APLY_RANG_PRC_1", "")
parameter("FID_TRGT_CLS_CODE", "11111111")
parameter("FID_TRGT_EXLS_CLS_CODE", "000000")
parameter("FID_RSFL_RATE2", "")
parameter("FID_RSFL_RATE1", "")
parameter("FID_INPUT_CNT_1", "0")
parameter("FID_INPUT_PRICE_1", "")
parameter("FID_INPUT_PRICE_2", "")
parameter("FID_VOL_CNT", "")
parameter("FID_INPUT_DATE_1", "")
// 상승/하락률 순위(HHPST01710000)일 경우 추가 파라미터
if (type.trId == "HHPST01710000") {
parameter("fid_diff_div_code", "00") // 00: 전일 대비
}
// [Step 3] 적용
params.forEach { (key, value) -> parameter(key, value) }
}
val body = response.body<RankingResponse>()
if (listOf(
RankingType.VOLUME_POWER,
RankingType.EXPECTED_RISE,
RankingType.COMPANY_TRADE
).contains(type)) {
println("${type.name} , ${body}" )
val rawJson = response.bodyAsText()
val body = try {
jsonParser.decodeFromString<RankingResponse>(rawJson)
} catch (e: Exception) {
println("[ERROR] ${type.title} 파싱 실패. Raw: $rawJson")
return Result.failure(e)
}
if (body.rt_cd == "0") {
Result.success(body.list ?: emptyList())
} else {
// rt_cd가 비어있다면 에러 메시지도 없을 수 있으므로 Raw Body 일부를 포함하여 예외 처리
val errorMsg = if (body.msg1.isNotBlank()) body.msg1 else "응답 코드 없음 (Raw: $rawJson)"
Result.failure(Exception(errorMsg))
}
if (body.rt_cd == "0") Result.success(body.list) else Result.failure(Exception(body.msg1))
} catch (e: Exception) {
if (listOf(
RankingType.VOLUME_POWER,
RankingType.EXPECTED_RISE,
RankingType.COMPANY_TRADE
).contains(type)) {
println("${type.name} , ${e.message}" )
}
Result.failure(e)
}
}
+18 -5
View File
@@ -13,6 +13,7 @@ import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.Url
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import model.DartFinancialResponse
@@ -36,17 +37,29 @@ object NewsService {
suspend fun fetchAndIngestNews(corpInfo: CorpInfo) {
val clientId = "CqXQXHO3h0kqtYsXkePY" // 설정에서 가져오도록 수정 필요
val clientSecret = "DODCxb1M4Z"
var qlist = listOf<String>("${corpInfo.stockName} 분석","${corpInfo.stockName}[${corpInfo.stockCode}]", "${corpInfo.cName} 최근 동향", "${corpInfo.cName}")
qlist.forEach { query ->
val qlistNews = listOf(
"${corpInfo.stockName} 주가",
"${corpInfo.stockName} 실적",
"${corpInfo.stockName} 공시",
"${corpInfo.stockName} 이벤트"
)
val qlistCorpTrend = listOf(
"${corpInfo.cName} 최근 동향",
"${corpInfo.cName} 이슈",
"${corpInfo.cName} 투자",
"${corpInfo.cName} 실적"
)
(qlistNews + qlistCorpTrend).forEach { query ->
try {
val response: NaverNewsResponse = client.get("https://openapi.naver.com/v1/search/news.json") {
parameter("query", query)
parameter("display", 3) // 최근 10개 뉴스
parameter("sort", "sim") // 유사도 순 (또는 date 발간순)
parameter("display", 5) // 최근 10개 뉴스
parameter("sort", "date") // 유사도 순 (또는 date 발간순)
header("X-Naver-Client-Id", clientId)
header("X-Naver-Client-Secret", clientSecret)
}.body()
SafeScraper.scrapeParallel(corpInfo,response.items)
SafeScraper.scrapeParallel(corpInfo,response.items.sortedBy { it.pubDate }.distinctBy { Url(it.originallink).host }.take(5) )
} catch (e: Exception) {
println("❌ 뉴스 가져오기 실패: ${e.message}")
}