package network import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIOEngineConfig import io.ktor.client.plugins.contentnegotiation.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.forms.FormDataContent import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.ContentType.Application.Json import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.http.Url import io.ktor.http.contentType import io.ktor.network.tls.TLSConfigBuilder import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import model.DartFinancialResponse import model.KisSession import model.NaverNewsResponse import service.SafeScraper import service.UrlCacheManager import java.io.BufferedReader import java.io.InputStreamReader import java.net.URL import java.net.URLEncoder import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import java.util.Locale import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import kotlin.Double object NewsService { private val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } install(Logging) { logger = Logger.DEFAULT level = LogLevel.NONE } } suspend fun fetchAndIngestNews(corpInfo: CorpInfo) { val clientId = KisSession.config.nAppKey // 설정에서 가져오도록 수정 필요 val clientSecret = KisSession.config.nSecretKey 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 formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH) val today = ZonedDateTime.now().toLocalDate() // 오늘 날짜 정보 val response: NaverNewsResponse = client.get("https://openapi.naver.com/v1/search/news.json") { parameter("query", query) parameter("display", 4) // 최근 10개 뉴스 parameter("sort", "date") // 유사도 순 (또는 date 발간순) header("X-Naver-Client-Id", clientId) header("X-Naver-Client-Secret", clientSecret) }.body() val todayItems = response.items.filter { item -> try { val pubDate = ZonedDateTime.parse(item.pubDate, formatter) pubDate.toLocalDate() == today // 날짜가 오늘과 일치하는지 확인 } catch (e: Exception) { false } } // 중복 호스트 제거 및 최종 2건 선택 val finalItems = todayItems .distinctBy { Url(it.originallink).host } .take(2) if (finalItems.isNotEmpty()) { SafeScraper.scrapeParallel(corpInfo, finalItems) } } catch (e: Exception) { println("❌ 뉴스 가져오기 실패: ${e.message}") } } } suspend fun fetchFinancialGrowth(corpCode: String?): String { if (corpCode != null) { val apiKey = KisSession.config.dAppKey // 단일회사 주요계정 API (재무상태표, 손익계산서 주요 항목) val url = "https://opendart.fss.or.kr/api/fnlttSinglAcnt.json?crtfc_key=$apiKey&corp_code=$corpCode&bsns_year=2024&reprt_code=11011" return try { val response = client.get(url).body() val accounts = response.list ?: return "재무 데이터 없음" var buffer : StringBuffer = StringBuffer() buffer.append("[재무 분석 데이터]").append("\n") response.list.forEach { it buffer.append("${it.account_nm} (당기)${it.thstrm_amount} (전기)${it.frmtrm_amount}").append("\n") } return buffer.toString() } catch (e: Exception) { "재무 API 연동 실패: ${e.message}" } } else { return "" } } suspend fun sendTelegramMessage(data: String) { Thread { try { var chatId = KisSession.tradeConfig.tlg_id println("sendTelegramMessage $chatId") sendViaSystemCurl("https://lunaticbum.kr/tlg/sendToMe.bjx",chatId,data) } catch (e: Exception) { e.printStackTrace() } }.start() } fun sendViaSystemCurl(url : String, chatId: String, message: String) { try { // 메시지 내 공백이나 한글이 깨지지 않도록 인코딩 (필수) val encodedMessage = URLEncoder.encode(message, "UTF-8") // OS 확인 val isWindows = System.getProperty("os.name").lowercase().contains("win") val command = if (isWindows) { // 윈도우용: 큰따옴표 이스케이프에 주의해야 합니다. val jsonBody = "{\"id\":\"$chatId\",\"message\":\"$encodedMessage\"}" listOf("cmd", "/c", "curl -s -X POST $url -H \"Content-Type: application/json\" -d \"$jsonBody\"") } else { // 맥/리눅스용: 홑따옴표를 사용하여 JSON 구조를 보호합니다. val jsonBody = "{\"id\":\"$chatId\",\"message\":\"$encodedMessage\"}" listOf("curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/json", "-d", jsonBody) } val process = ProcessBuilder(command) .redirectErrorStream(true) // 에러 출력(stderr)을 표준 출력(stdout)으로 합침 .start() // 프로세스의 출력을 읽어오는 블록 BufferedReader(InputStreamReader(process.inputStream)).use { reader -> val output = StringBuilder() var line: String? while (reader.readLine().also { line = it } != null) { output.append(line).append("\n") } val exitCode = process.waitFor() // 프로세스가 종료될 때까지 대기 println("--- Telegram Curl Log Start ---") println("Exit Code: $exitCode") // 0이면 성공, 그 외는 curl 에러 코드 println("Response:\n$output") println("--- Telegram Curl Log End ---") } } catch (e: Exception) { println("시스템 명령어 실행 중 예외 발생: ${e.message}") e.printStackTrace() } } }