diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index e6c120a..2ea0a59 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -328,6 +328,7 @@ fun main() = application { // DashboardScreen() } AppScreen.TradingDecision -> { + TradingDecisionLog() } } diff --git a/src/main/kotlin/database/DatabaseFactory.kt b/src/main/kotlin/database/DatabaseFactory.kt index 97f83b5..fc67e2f 100644 --- a/src/main/kotlin/database/DatabaseFactory.kt +++ b/src/main/kotlin/database/DatabaseFactory.kt @@ -1,10 +1,16 @@ import androidx.compose.runtime.mutableStateListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import model.AppConfig import model.TradingDecision +import network.NewsService import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.javatime.datetime +import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync import org.jetbrains.exposed.sql.transactions.transaction import report.TradingReportManager import report.TradingReportService @@ -509,11 +515,21 @@ object TradingLogStore { decisionLogs.add( LogEntry( time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")), - stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}][]", + stockName = "${tradingDecision.stockName}[${tradingDecision.currentPrice}]", decision = decision, confidence = tradingDecision.confidence, reason = log - ) + ).apply { + CoroutineScope(Dispatchers.Default).launch { + println("CALLED sendTelegramMessage -1") + if (decision.contains("WATCH") || ((tradingDecision.investmentGrade?.ordinal + ?: 0) < 2) + ) { + println("CALLED sendTelegramMessage OK") + NewsService.sendTelegramMessage("${this@apply.decision} ${tradingDecision.stockName}[${tradingDecision.currentPrice}] ${log}") + } + } + } ) } } @@ -545,8 +561,14 @@ object TradingLogStore { decision = "NOTICE", confidence = 100.0, reason = log - ) + ).apply { + CoroutineScope(Dispatchers.Default).launch { + println("CALLED sendTelegramMessage") + NewsService.sendTelegramMessage("${this@apply.decision}$name[$code] ${log}") + } + } ) + } } diff --git a/src/main/kotlin/model/AppConfig.kt b/src/main/kotlin/model/AppConfig.kt index 5a72031..4d15174 100644 --- a/src/main/kotlin/model/AppConfig.kt +++ b/src/main/kotlin/model/AppConfig.kt @@ -240,6 +240,7 @@ class TradeConfig { var start_buy_time : String = "08:55" var end_buy_time : String = "15:10" var enableOverSea : Boolean = false + var tlg_id : String = "" } diff --git a/src/main/kotlin/network/KisTradeService.kt b/src/main/kotlin/network/KisTradeService.kt index 0024a98..46dabab 100644 --- a/src/main/kotlin/network/KisTradeService.kt +++ b/src/main/kotlin/network/KisTradeService.kt @@ -45,7 +45,7 @@ object KisTradeService { // [수정] 모든 로그(Headers + Body)를 찍도록 설정 install(Logging) { logger = Logger.DEFAULT - level = LogLevel.NONE + level = LogLevel.ALL } } diff --git a/src/main/kotlin/network/NewsService.kt b/src/main/kotlin/network/NewsService.kt index 742c1b5..31c5fc3 100644 --- a/src/main/kotlin/network/NewsService.kt +++ b/src/main/kotlin/network/NewsService.kt @@ -17,10 +17,14 @@ 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 @@ -29,10 +33,16 @@ 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 { @@ -122,4 +132,62 @@ object NewsService { 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\":\"$message\"}" + 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() + } + } + } diff --git a/src/main/kotlin/ui/SettingsScreen.kt b/src/main/kotlin/ui/SettingsScreen.kt index ca3aac4..7f3b4b3 100644 --- a/src/main/kotlin/ui/SettingsScreen.kt +++ b/src/main/kotlin/ui/SettingsScreen.kt @@ -45,6 +45,7 @@ import java.awt.Toolkit import java.awt.datatransfer.DataFlavor import java.io.File import androidx.compose.ui.input.key.* +import network.NewsService fun getPastedPathFromClipboard(): String? { val clipboard = Toolkit.getDefaultToolkit().systemClipboard @@ -138,6 +139,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { SystemSleepPreventer.wakeDisplay() // 모니터 켜기 statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다." authenticateAndStart() + break // 성공하면 루프 탈출 } } diff --git a/src/main/kotlin/ui/TradingDecisionLog.kt b/src/main/kotlin/ui/TradingDecisionLog.kt index 128f94d..4c186b2 100644 --- a/src/main/kotlin/ui/TradingDecisionLog.kt +++ b/src/main/kotlin/ui/TradingDecisionLog.kt @@ -42,10 +42,13 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import model.ConfigIndex import model.KisSession import network.KisTradeService +import network.NewsService import network.StockUniverseLoader import service.AutoTradingManager import java.io.File @@ -62,7 +65,13 @@ fun TradingDecisionLog() { var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) } val tradeConfig by remember { KisSession.tradeConfig = KisSession.loadTradeConfig() + CoroutineScope(Dispatchers.Default).launch { + println("CALLED sendTelegramMessage -1") + val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul")) + NewsService.sendTelegramMessage("⏰ 자동 실행 시간(${now.hour}:${now.minute})입니다. 시스템을 가동합니다.") + } mutableStateOf(KisSession.tradeConfig) + } LaunchedEffect(AutoTradingManager.llmAnalyser) { llmAnalyser = AutoTradingManager.llmAnalyser