....
This commit is contained in:
parent
eb2dc0efd0
commit
6d340b7dc0
@ -41,6 +41,7 @@ import service.LlamaServerManager
|
|||||||
import network.NewsService
|
import network.NewsService
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import service.AutoTradingManager
|
import service.AutoTradingManager
|
||||||
|
import service.AutoTradingManager.isSystemCleanedUpToday
|
||||||
import service.SystemSleepPreventer
|
import service.SystemSleepPreventer
|
||||||
import service.TradingDecisionCallback
|
import service.TradingDecisionCallback
|
||||||
import ui.DashboardScreen
|
import ui.DashboardScreen
|
||||||
@ -151,7 +152,8 @@ fun main() = application {
|
|||||||
onAuthSuccess = {
|
onAuthSuccess = {
|
||||||
// 2. 설정 및 인증 완료 시점의 처리
|
// 2. 설정 및 인증 완료 시점의 처리
|
||||||
val config = KisSession.config
|
val config = KisSession.config
|
||||||
|
AutoTradingManager.isSystemReadyToday = true
|
||||||
|
AutoTradingManager.isSystemCleanedUpToday = false
|
||||||
// LLM 서버 시작 (설정된 모델 경로 사용)
|
// LLM 서버 시작 (설정된 모델 경로 사용)
|
||||||
if (config.modelPath.isNotEmpty()) {
|
if (config.modelPath.isNotEmpty()) {
|
||||||
LlamaServerManager.startServer(binPath, config.modelPath,port = 8080)
|
LlamaServerManager.startServer(binPath, config.modelPath,port = 8080)
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import model.TokenRequest
|
|||||||
import model.TokenResponse
|
import model.TokenResponse
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
class KisAuthService {
|
object KisAuthService {
|
||||||
private val client = HttpClient(CIO) {
|
private val client = HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import model.RealTimeTrade
|
|||||||
import util.AesCrypto
|
import util.AesCrypto
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class KisWebSocketManager {
|
object KisWebSocketManager {
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
pingInterval = 20_000 // 20초마다 표준 웹소켓 핑 전송 (서버-클라이언트 연결 유지 도움)
|
pingInterval = 20_000 // 20초마다 표준 웹소켓 핑 전송 (서버-클라이언트 연결 유지 도움)
|
||||||
@ -23,7 +23,7 @@ class KisWebSocketManager {
|
|||||||
private var session: DefaultClientWebSocketSession? = null
|
private var session: DefaultClientWebSocketSession? = null
|
||||||
private val isConnected = AtomicBoolean(false)
|
private val isConnected = AtomicBoolean(false)
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
private var connectJob: Job? = null
|
||||||
// 콜백 리스너
|
// 콜백 리스너
|
||||||
var onPriceUpdate: ((RealTimeTrade) -> Unit)? = null
|
var onPriceUpdate: ((RealTimeTrade) -> Unit)? = null
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ class KisWebSocketManager {
|
|||||||
suspend fun connect() {
|
suspend fun connect() {
|
||||||
if (isConnected.get()) return
|
if (isConnected.get()) return
|
||||||
val url = if (KisSession.config.isSimulation) "ops.koreainvestment.com:31000" else "ops.koreainvestment.com:21000"
|
val url = if (KisSession.config.isSimulation) "ops.koreainvestment.com:31000" else "ops.koreainvestment.com:21000"
|
||||||
|
connectJob?.cancelAndJoin()
|
||||||
scope.launch {
|
connectJob = scope.launch {
|
||||||
while (isActive) { // 재연결을 위한 루프 추가
|
while (isActive) { // 재연결을 위한 루프 추가
|
||||||
try {
|
try {
|
||||||
client.webSocket(method = HttpMethod.Get, host = url.split(":")[0], port = url.split(":")[1].toInt(), path = "/last_price") {
|
client.webSocket(method = HttpMethod.Get, host = url.split(":")[0], port = url.split(":")[1].toInt(), path = "/last_price") {
|
||||||
@ -192,4 +192,14 @@ class KisWebSocketManager {
|
|||||||
activeSubscriptions.addAll(requiredCodes)
|
activeSubscriptions.addAll(requiredCodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun disconnect() {
|
||||||
|
println("🔌 웹소켓 연결 종료 중...")
|
||||||
|
isConnected.set(false)
|
||||||
|
connectJob?.cancelAndJoin() // 루프 자체를 중단
|
||||||
|
session?.close()
|
||||||
|
session = null
|
||||||
|
connectJob = null
|
||||||
|
println("🛑 웹소켓 완전 종료")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import TradingDecision
|
import TradingDecision
|
||||||
|
import getLlamaBinPath
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -23,7 +24,9 @@ import model.UnifiedBalance
|
|||||||
import network.DartCodeManager
|
import network.DartCodeManager
|
||||||
import network.FinancialMapper
|
import network.FinancialMapper
|
||||||
import network.FinancialStatement
|
import network.FinancialStatement
|
||||||
|
import network.KisAuthService
|
||||||
import network.KisTradeService
|
import network.KisTradeService
|
||||||
|
import network.KisWebSocketManager
|
||||||
import util.MarketUtil
|
import util.MarketUtil
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
@ -116,83 +119,196 @@ object AutoTradingManager {
|
|||||||
delay(200) // API 호출 부하 방지
|
delay(200) // API 호출 부하 방지
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var isSystemReadyToday = false
|
||||||
|
var isSystemCleanedUpToday = false
|
||||||
|
private var lastRetryTime = 0L
|
||||||
|
val binPath = getLlamaBinPath()
|
||||||
|
|
||||||
|
suspend fun tryRefreshToken() {
|
||||||
|
try {
|
||||||
|
// 2분 간격 재시도 로직 (처음 실행 시에는 lastRetryTime이 0이므로 즉시 실행)
|
||||||
|
if (currentTimeMillis - lastRetryTime >= 2 * 60 * 1000L) {
|
||||||
|
lastRetryTime = currentTimeMillis
|
||||||
|
|
||||||
|
println("🌅 [System] 오전 8시 업무 시작 준비 시도...")
|
||||||
|
SystemSleepPreventer.wakeDisplay() // 모니터 깨우기
|
||||||
|
|
||||||
|
val authSuccess = KisAuthService.refreshAllTokens()
|
||||||
|
val wsSuccess = KisTradeService.refreshWebsocketKey()
|
||||||
|
|
||||||
|
if (authSuccess && wsSuccess) {
|
||||||
|
println("✅ [System] 토큰 갱신 성공. AI 서버를 기동합니다.")
|
||||||
|
// 서버 시작 로직 실행 (Main.kt에 있던 로직 활용)
|
||||||
|
val config = KisSession.config
|
||||||
|
// LLM 서버 시작 (설정된 모델 경로 사용)
|
||||||
|
if (config.modelPath.isNotEmpty()) {
|
||||||
|
LlamaServerManager.startServer(binPath, config.modelPath,port = 8080)
|
||||||
|
}
|
||||||
|
if (config.embedModelPath.isNotEmpty()) {
|
||||||
|
LlamaServerManager.startServer(binPath, config.embedModelPath, port = 8081)
|
||||||
|
}
|
||||||
|
KisWebSocketManager.connect()
|
||||||
|
isSystemReadyToday = true
|
||||||
|
} else {
|
||||||
|
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
|
var currentTimeMillis = System.currentTimeMillis()
|
||||||
|
var waitTime = 0.2
|
||||||
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||||
discoveryJob = scope.launch {
|
discoveryJob = scope.launch {
|
||||||
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
|
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
|
now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
|
currentTimeMillis = System.currentTimeMillis()
|
||||||
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
|
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
|
||||||
|
// if (now.minute % 5 == 0) {
|
||||||
withTimeout(CYCLE_TIMEOUT) {
|
// SystemSleepPreventer.sleepDisplay()
|
||||||
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
// } else {
|
||||||
|
// SystemSleepPreventer.wakeDisplay()
|
||||||
// [프로세스 1] 장 마감 및 잔고 체크
|
// }
|
||||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
when {
|
||||||
//&& now.isBefore(LocalTime.of(15, 30))
|
//장중
|
||||||
if (now.isAfter(LocalTime.of(15, 20)) ) {
|
now.isBefore(LocalTime.of(16, 0)) && now.isAfter(LocalTime.of(8, 50)) -> {
|
||||||
executeClosingLiquidation(tradeService)
|
waitTime = 0.2
|
||||||
return@withTimeout
|
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
|
||||||
}
|
// 토큰 중 하나라도 만료 5분 전이거나 비어있다면 다시 준비 상태로 전환
|
||||||
// addToReanalysis(RankingStock(mksc_shrn_iscd = ,hts_kor_isnm = ))
|
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||||
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
if (isSystemReadyToday) {
|
||||||
|
println("⚠️ [System] 토큰 만료 감지. 재발급 프로세스를 가동합니다.")
|
||||||
balance?.let { resumePendingSellOrders(tradeService,it) }
|
isSystemReadyToday = false
|
||||||
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
KisWebSocketManager.disconnect()
|
||||||
val myHoldings = balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet() ?: emptySet()
|
tryRefreshToken()
|
||||||
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
}
|
||||||
// [프로세스 2] 후보군 수집
|
|
||||||
if (remainingCandidates.isEmpty()) {
|
|
||||||
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
|
|
||||||
println("후보군 총 개수 : $size")
|
|
||||||
}.filter {
|
|
||||||
val rate = it.prdy_ctrt.toDouble()
|
|
||||||
val corpInfo = DartCodeManager.getCorpCode(it.code)
|
|
||||||
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
|
|
||||||
// if (isOk) {println("${it.name} : ${it.prdy_ctrt}")}
|
|
||||||
if (corpInfo?.cName.isNullOrEmpty()) {
|
|
||||||
false
|
|
||||||
}else {
|
|
||||||
isOk
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.filter { !it.name.contains("호스팩", true) }
|
withTimeout(CYCLE_TIMEOUT) {
|
||||||
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
|
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
if (reanalysisList.isNotEmpty()) {
|
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
candidates.addAll(reanalysisList.asReversed())
|
if (now.isAfter(LocalTime.of(15, 20))) {
|
||||||
|
executeClosingLiquidation(tradeService)
|
||||||
|
return@withTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
val balance = tradeService.fetchIntegratedBalance().getOrNull()
|
||||||
|
balance?.let { resumePendingSellOrders(tradeService, it) }
|
||||||
|
val myCash = balance?.deposit?.replace(",", "")?.toLongOrNull() ?: 0L
|
||||||
|
val myHoldings =
|
||||||
|
balance?.holdings?.filter { it.quantity.toInt() > 0 }?.map { it.code }?.toSet()
|
||||||
|
?: emptySet()
|
||||||
|
val pendingStocks = DatabaseFactory.findAllMonitoringTrades().map { it.code }
|
||||||
|
// [프로세스 2] 후보군 수집
|
||||||
|
if (remainingCandidates.isEmpty()) {
|
||||||
|
val candidates: MutableList<RankingStock> = fetchCandidates(tradeService).apply {
|
||||||
|
println("후보군 총 개수 : $size")
|
||||||
|
}.filter {
|
||||||
|
val rate = it.prdy_ctrt.toDouble()
|
||||||
|
val corpInfo = DartCodeManager.getCorpCode(it.code)
|
||||||
|
val isOk = (rate > 0 && rate < 15) || (rate < 0 && rate > -15)
|
||||||
|
if (corpInfo?.cName.isNullOrEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
isOk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter { !it.name.contains("호스팩", true) }
|
||||||
|
.sortedBy { (it.prdy_ctrt.toDoubleOrNull() ?: 0.0) }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
if (reanalysisList.isNotEmpty()) {
|
||||||
|
candidates.addAll(reanalysisList.asReversed())
|
||||||
|
}
|
||||||
|
reanalysisList.clear()
|
||||||
|
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }
|
||||||
|
.distinctBy { it.code })
|
||||||
|
} else {
|
||||||
|
println("미확인 데이터 ${remainingCandidates.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [프로세스 3] 종목별 순회 분석
|
||||||
|
var totalCount = remainingCandidates.size
|
||||||
|
println("후보군 조건 충족 총 개수 : ${totalCount}")
|
||||||
|
val iterator = remainingCandidates.iterator()
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
if (now.minute % 2 == 0) {
|
||||||
|
// SystemSleepPreventer.sleepDisplay()
|
||||||
|
} else {
|
||||||
|
// SystemSleepPreventer.wakeDisplay()
|
||||||
|
}
|
||||||
|
totalCount--
|
||||||
|
val stock = iterator.next()
|
||||||
|
try {
|
||||||
|
processSingleStock(stock, myCash, tradeService, callback)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
|
||||||
|
} finally {
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
println("남은 후보군 개수 : ${totalCount}")
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||||
}
|
}
|
||||||
reanalysisList.clear()
|
|
||||||
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks }.distinctBy { it.code })
|
|
||||||
} else {
|
|
||||||
println("미확인 데이터 ${remainingCandidates.size}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//장외
|
||||||
|
now.isAfter(LocalTime.of(18, 0)) || now.isBefore(LocalTime.of(8, 50)) -> {
|
||||||
|
when {
|
||||||
|
(now.hour == 0 && now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
|
||||||
|
waitTime = 10.0
|
||||||
|
isSystemReadyToday = false
|
||||||
|
isSystemCleanedUpToday = false
|
||||||
|
}
|
||||||
|
|
||||||
// [프로세스 3] 종목별 순회 분석
|
(now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
|
||||||
var totalCount = remainingCandidates.size
|
waitTime = 3.0
|
||||||
println("후보군 조건 충족 총 개수 : ${totalCount}")
|
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||||
val iterator = remainingCandidates.iterator()
|
KisWebSocketManager.disconnect()
|
||||||
|
tryRefreshToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
(now.isAfter(LocalTime.of(18, 0))) -> {
|
||||||
totalCount--
|
try {
|
||||||
val stock = iterator.next()
|
waitTime = 5.0
|
||||||
try {
|
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
|
||||||
processSingleStock(stock, myCash, tradeService, callback)
|
if (!isSystemCleanedUpToday) {
|
||||||
// 성공적으로 처리(또는 분석 완료) 후 리스트에서 제거
|
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
|
||||||
} catch (e: Exception) {
|
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
|
||||||
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
|
KisWebSocketManager.disconnect()
|
||||||
// 오류 시 리스트에 남겨둘지, 제거할지 결정
|
//isSystemReadyToday = false
|
||||||
// (심각한 에러면 remove하고 다음 루프에서 다시 받는게 안전)
|
if (LlamaServerManager.stopAll()) {
|
||||||
} finally {
|
isSystemCleanedUpToday = true
|
||||||
iterator.remove()
|
}
|
||||||
|
}
|
||||||
|
println("✅ [System] 오늘의 모든 정리가 완료되었습니다.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(now.isAfter(LocalTime.of(18, 15)) && now.minute % 15 == 0) -> {
|
||||||
|
try {
|
||||||
|
waitTime = 5.0
|
||||||
|
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
waitTime = 5.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println("남은 후보군 개수 : ${totalCount}")
|
|
||||||
delay(100)
|
|
||||||
}
|
}
|
||||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
else ->{
|
||||||
|
waitTime = 3.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: TimeoutCancellationException) {
|
} catch (e: TimeoutCancellationException) {
|
||||||
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
println("⏳ [Cycle Timeout] 사이클이 너무 길어져 초기화 후 재시작합니다.")
|
||||||
@ -200,8 +316,7 @@ object AutoTradingManager {
|
|||||||
println("⚠️ [Loop Error] ${e.message}")
|
println("⚠️ [Loop Error] ${e.message}")
|
||||||
delay(1500)
|
delay(1500)
|
||||||
}
|
}
|
||||||
|
waitForNextCycle(waitTime)
|
||||||
waitForNextCycle(0.2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,12 +417,12 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun waitForNextCycle(minutes: Double) {
|
private suspend fun waitForNextCycle(minutes: Double) {
|
||||||
println("💤 대기 모드 진입...")
|
println("💤 대기 모드 진입... $minutes")
|
||||||
val endWait = System.currentTimeMillis() + (minutes * 60 * 1000L)
|
val endWait = System.currentTimeMillis() + (minutes * 60 * 1000L)
|
||||||
while (System.currentTimeMillis() < endWait && isRunning()) {
|
while (System.currentTimeMillis() < endWait && isRunning()) {
|
||||||
lastTickTime.set(System.currentTimeMillis()) // 대기 중에도 Watchdog에 생존 신고
|
lastTickTime.set(System.currentTimeMillis()) // 대기 중에도 Watchdog에 생존 신고
|
||||||
println("💤 대기 모드 상태 확인...")
|
println("💤 대기 모드 상태 확인...")
|
||||||
delay(1000)
|
delay(if(minutes > 3.0 ) 10000 else 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,11 +87,37 @@ object LlamaServerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopAll() {
|
fun stopAll(): Boolean {
|
||||||
|
var allStopped = true // 모든 프로세스 종료 여부를 추적하는 플래그
|
||||||
|
|
||||||
processes.forEach { (port, process) ->
|
processes.forEach { (port, process) ->
|
||||||
process.destroy()
|
try {
|
||||||
println("🛑 AI 서버 종료 (Port: $port)")
|
process.destroy() // 1차: 부드러운 종료 시도
|
||||||
|
|
||||||
|
// 2차: 최대 3초 대기 후 종료되지 않으면 강제 종료
|
||||||
|
if (!process.waitFor(3, java.util.concurrent.TimeUnit.SECONDS)) {
|
||||||
|
process.destroyForcibly() // 강제 사살
|
||||||
|
|
||||||
|
// 강제 종료 후에도 프로세스가 살아있는지 최종 확인
|
||||||
|
if (process.isAlive) {
|
||||||
|
println("❌ [Server $port] 강제 종료 명령 후에도 프로세스가 살아있습니다.")
|
||||||
|
allStopped = false
|
||||||
|
} else {
|
||||||
|
println("⚠️ [Server $port] 응답이 없어 강제 종료되었습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("🛑 [Server $port] 정상 종료되었습니다.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("❌ [Server $port] 종료 중 오류: ${e.message}")
|
||||||
|
allStopped = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
processes.clear()
|
|
||||||
|
if (allStopped) {
|
||||||
|
processes.clear() // 모든 프로세스가 성공적으로 종료되거나 리스트에서 제거될 준비가 된 경우
|
||||||
|
}
|
||||||
|
|
||||||
|
return allStopped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,14 +7,61 @@ import ch.qos.logback.classic.Logger
|
|||||||
|
|
||||||
object SystemSleepPreventer {
|
object SystemSleepPreventer {
|
||||||
private var process: Process? = null
|
private var process: Process? = null
|
||||||
|
fun checkAndRequestAccessibility() {
|
||||||
|
if (!hasAccessibilityPermission()) {
|
||||||
|
println("⚠️ [System] 접근성 권한이 없습니다. 설정창을 엽니다.")
|
||||||
|
openAccessibilitySettings()
|
||||||
|
} else {
|
||||||
|
println("✅ [System] 접근성 권한이 확인되었습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제로 가벼운 이벤트를 발생시켜 권한 유무를 확인하는 함수
|
||||||
|
*/
|
||||||
|
private fun hasAccessibilityPermission(): Boolean {
|
||||||
|
return try {
|
||||||
|
// System Events에 이름을 묻는 아주 가벼운 명령을 실행합니다.
|
||||||
|
val process = Runtime.getRuntime().exec(
|
||||||
|
arrayOf("osascript", "-e", "tell application \"System Events\" to get name")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 권한이 없으면 팝업이 뜨며 대기할 수 있으므로, 아주 짧은 타임아웃을 줍니다.
|
||||||
|
val exited = process.waitFor(1500, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
if (!exited) {
|
||||||
|
// 타임아웃 발생 시, 시스템이 권한 승인 팝업을 띄우고 대기 중일 확률이 높습니다.
|
||||||
|
process.destroyForcibly()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에러 스트림을 읽어 권한 거부 관련 메시지가 있는지 확인합니다.
|
||||||
|
val errorStream = process.errorStream.bufferedReader().readText()
|
||||||
|
val isDenied = errorStream.contains("not allowed") || process.exitValue() != 0
|
||||||
|
|
||||||
|
!isDenied
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ [Permission] 체크 중 오류 발생: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAccessibilitySettings() {
|
||||||
|
try {
|
||||||
|
Runtime.getRuntime().exec(arrayOf(
|
||||||
|
"open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
||||||
*/
|
*/
|
||||||
fun start() {
|
fun start() {
|
||||||
val root = LoggerFactory.getLogger("Exposed") as Logger
|
val root = LoggerFactory.getLogger("Exposed") as Logger
|
||||||
root.level = Level.ERROR
|
root.level = Level.ERROR
|
||||||
|
checkAndRequestAccessibility()
|
||||||
if (process?.isAlive == true) return
|
if (process?.isAlive == true) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -27,6 +74,36 @@ object SystemSleepPreventer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모니터를 즉시 잠자기 모드로 전환
|
||||||
|
*/
|
||||||
|
fun sleepDisplay() {
|
||||||
|
try {
|
||||||
|
// pmset을 이용해 디스플레이를 즉시 끔
|
||||||
|
Runtime.getRuntime().exec("pmset displaysleepnow")
|
||||||
|
println("🌙 [System] 오후 6시 30분: 모니터를 잠자기 모드로 전환합니다.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ 모니터 잠자기 실패: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 움직임을 시뮬레이션하거나 디스플레이 깨우기 명령 실행
|
||||||
|
*/
|
||||||
|
fun wakeDisplay() {
|
||||||
|
try {
|
||||||
|
// caffeinate를 다시 실행하여 깨우거나,
|
||||||
|
// 쉘 명령어로 키 입력을 시뮬레이션하여 화면을 깨움
|
||||||
|
Runtime.getRuntime().exec(
|
||||||
|
arrayOf("caffeinate", "-u", "-t", "3600")
|
||||||
|
)
|
||||||
|
// Runtime.getRuntime().exec(arrayOf("osascript", "-e", "tell application \"System Events\" to key code 123"))
|
||||||
|
println("☀️ 오전 8시: 모니터를 깨웁니다.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("⚠️ 모니터 깨우기 실패: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앱 종료 시 프로세스 함께 종료
|
* 앱 종료 시 프로세스 함께 종료
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import kotlin.collections.mutableListOf
|
|||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen() {
|
fun DashboardScreen() {
|
||||||
val tradeService = remember { KisTradeService }
|
val tradeService = remember { KisTradeService }
|
||||||
val wsManager = remember { KisWebSocketManager() }
|
val wsManager = remember { KisWebSocketManager }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var selectedStockCode by remember { mutableStateOf("") }
|
var selectedStockCode by remember { mutableStateOf("") }
|
||||||
var selectedStockName by remember { mutableStateOf("") }
|
var selectedStockName by remember { mutableStateOf("") }
|
||||||
|
|||||||
@ -122,7 +122,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
|||||||
// 1. KisSession.config 업데이트 및 DB 저장
|
// 1. KisSession.config 업데이트 및 DB 저장
|
||||||
KisSession.config = config
|
KisSession.config = config
|
||||||
DatabaseFactory.saveConfig(config)
|
DatabaseFactory.saveConfig(config)
|
||||||
val authService = KisAuthService()
|
val authService = KisAuthService
|
||||||
val tradeService = KisTradeService
|
val tradeService = KisTradeService
|
||||||
val authSuccess = authService.refreshAllTokens()
|
val authSuccess = authService.refreshAllTokens()
|
||||||
val wsKeySuccess = tradeService.refreshWebsocketKey()
|
val wsKeySuccess = tradeService.refreshWebsocketKey()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user