...
This commit is contained in:
parent
b289b4d9a0
commit
af0dc6b15f
@ -21,6 +21,7 @@ import model.DartFinancialResponse
|
||||
import model.KisSession
|
||||
import model.NaverNewsResponse
|
||||
import service.DynamicNewsScraper
|
||||
import service.FinancialAnalyzer
|
||||
import service.SafeScraper
|
||||
import service.UrlCacheManager
|
||||
import kotlin.Double
|
||||
@ -94,7 +95,7 @@ object NewsService {
|
||||
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")
|
||||
buffer.append("${it.account_nm} (당기)${it.thstrm_amount} (전기)${it.frmtrm_amount}").append("\n")
|
||||
}
|
||||
return buffer.toString()
|
||||
} catch (e: Exception) {
|
||||
@ -114,6 +115,7 @@ object FinancialMapper {
|
||||
if (rawText.isBlank()) {
|
||||
return FinancialStatement()
|
||||
}
|
||||
// println(rawText)
|
||||
val currentValues = extractYearlyValues(rawText, "당기")
|
||||
val previousValues = extractYearlyValues(rawText, "전기")
|
||||
|
||||
@ -148,17 +150,23 @@ object FinancialMapper {
|
||||
quickRatio = quickRatio,
|
||||
isOperatingProfitPositive = opCurrent > 0,
|
||||
isNetIncomePositive = niCurrent > 0
|
||||
)
|
||||
).apply {
|
||||
println("당기순이익: ${niCurrent} , isSafetyBeltMet ${FinancialAnalyzer.isSafetyBeltMet(this)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractYearlyValues(text: String, type: String): Map<String, Double> {
|
||||
val result = mutableMapOf<String, Double>()
|
||||
// 정규식 설명: 항목명 뒤의 (당기/전기) 괄호 안의 숫자와 콤마를 찾아 숫자로 변환
|
||||
val regex = Regex("""([가-힣\s()]+)\s\(?$type\)?([-0-9,.]+)""")
|
||||
|
||||
// 핵심 수정: 항목명 뒤에 (당기) 또는 (전기)가 오고, 그 직후의 숫자(마이너스, 쉼표 포함)를 캡처
|
||||
// 쉼표나 공백으로 끝나는 지점까지 찾습니다.
|
||||
val regex = Regex("""([가-힣\s()]+)\s\($type\)([-0-9,.]+)""")
|
||||
|
||||
regex.findAll(text).forEach { match ->
|
||||
val key = match.groupValues[1].trim()
|
||||
val value = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
result[key] = value
|
||||
// 숫자 내 쉼표 제거 후 Double 변환
|
||||
val rawValue = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0
|
||||
result[key] = rawValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -201,7 +201,6 @@ object RagService {
|
||||
val scores = technicalAnalyzer.calculateScores(financialScore)
|
||||
if (scores.avg() > 50) {
|
||||
result(tradingDecision, false)
|
||||
|
||||
tradingDecision.techSummary = technicalAnalyzer.generateComprehensiveReport()
|
||||
result(tradingDecision, false)
|
||||
|
||||
@ -217,9 +216,12 @@ object RagService {
|
||||
result(tradingDecision, false)
|
||||
result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true)
|
||||
} else {
|
||||
println("${corpInfo?.cName} : ${scores.toString()}")
|
||||
tradingDecision.confidence = 1.0
|
||||
result(tradingDecision, false)
|
||||
}
|
||||
} else {
|
||||
tradingDecision.confidence = 1.0
|
||||
result(tradingDecision, false)
|
||||
}
|
||||
}catch (e: Exception) {
|
||||
|
||||
@ -3,7 +3,6 @@ package service
|
||||
import AutoTradeItem
|
||||
import network.TradingDecision
|
||||
import TradingLogStore
|
||||
import TradingLogStore.decisionLogs
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -358,7 +357,7 @@ object AutoTradingManager {
|
||||
}
|
||||
|
||||
// 2. 메인 루프 실행
|
||||
runDiscoveryLoop(KisTradeService, globalCallback)
|
||||
runDiscoveryLoop(globalCallback)
|
||||
}
|
||||
|
||||
|
||||
@ -375,6 +374,7 @@ object AutoTradingManager {
|
||||
targetPrice = MarketUtil.roundToTickSize(targetPrice + MarketUtil.getTickSize(targetPrice))
|
||||
|
||||
println("🔄 [재주문] ${holding.name} (${holding.code}) 매도 목표 ${targetPrice} 미체결 매도 건 재주문 시도")
|
||||
// TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문[예상수익 : ${holding.profitRate}] ")
|
||||
tradeService.postOrder(
|
||||
stockCode = holding.code,
|
||||
qty = holding.availOrderCount,
|
||||
@ -384,8 +384,10 @@ object AutoTradingManager {
|
||||
// 4. 새로운 주문번호로 DB 업데이트 및 상태를 SELLING으로 유지
|
||||
// DatabaseFactory.updateStatusAndOrderNo(item.id!!, TradeStatus.SELLING, newOrderNo)
|
||||
println("✅ [재주문 완료] ${holding.name}: $newOrderNo")
|
||||
TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 완료[예상수익 : ${holding.profitRate}] ")
|
||||
}.onFailure {
|
||||
println("❌ [재주문 실패] ${holding.name}: ${it.message}")
|
||||
TradingLogStore.addSellLog(holding.code,targetPrice.toString(),"SELL","🎊 보유 주식 매도 주문 실패[${it.message}] ")
|
||||
// println("❌ [재주문 실패] ${holding.name}: ${it.message}")
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -437,9 +439,9 @@ object AutoTradingManager {
|
||||
var currentTimeMillis = System.currentTimeMillis()
|
||||
var waitTime = 0.2
|
||||
val H16 = LocalTime.of(16, 0)
|
||||
val H08M50 = LocalTime.of(8, 50)
|
||||
val H08M35 = LocalTime.of(8, 35)
|
||||
val H08M30 = LocalTime.of(8, 30)
|
||||
private fun runDiscoveryLoop(tradeService: KisTradeService, callback: TradingDecisionCallback) {
|
||||
private fun runDiscoveryLoop(callback: TradingDecisionCallback) {
|
||||
discoveryJob = scope.launch {
|
||||
println("🚀 [AutoTrading] 발굴 루프 시작: ${LocalDateTime.now()}")
|
||||
while (isActive) {
|
||||
@ -447,34 +449,11 @@ object AutoTradingManager {
|
||||
now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||
currentTimeMillis = System.currentTimeMillis()
|
||||
lastTickTime.set(System.currentTimeMillis()) // 생존 신고
|
||||
// [수정] 16시 이후이거나 8시 30분 이전이면 모든 로직 중단 및 초기화
|
||||
if (now.isAfter(H16) || now.isBefore(H08M30)) {
|
||||
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
|
||||
onMarketClosed?.invoke()
|
||||
|
||||
|
||||
KisWebSocketManager.disconnect()
|
||||
BrowserManager.closeIfIdle(0)
|
||||
LlamaServerManager.stopAll() // AI 서버 완전 종료
|
||||
TradingLogStore.clear()
|
||||
// Main.kt에 설정 화면으로 가라고 신호 전송
|
||||
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
||||
return@launch
|
||||
} else if (now.isAfter(H08M30) && now.isBefore(H08M50) && !isSystemReadyToday) {
|
||||
if (MarketUtil.canTradeToday()) {
|
||||
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
|
||||
tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true)
|
||||
} else {
|
||||
println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.")
|
||||
isSystemReadyToday = false
|
||||
delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여
|
||||
continue
|
||||
}
|
||||
}
|
||||
when {
|
||||
|
||||
//장중
|
||||
now.isBefore(H16) && now.isAfter(H08M50) -> {
|
||||
now.isAfter(H16) || now.isBefore(H08M35) -> {
|
||||
prepareMarketOpen(now)
|
||||
}
|
||||
now.isBefore(H16) && now.isAfter(H08M35) -> {
|
||||
waitTime = 0.2
|
||||
if (now.isAfter(LocalTime.of(8, 0)) && now.isBefore(LocalTime.of(15, 30))) {
|
||||
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||
@ -488,122 +467,18 @@ object AutoTradingManager {
|
||||
}
|
||||
withTimeout(CYCLE_TIMEOUT) {
|
||||
println("⏱️ [Cycle Start] ${LocalTime.now()}")
|
||||
|
||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||
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 }
|
||||
if (remainingCandidates.isEmpty()) {
|
||||
val stocks = StockUniverseLoader.loadUniverse().shuffled().take(100)
|
||||
println("✅ 총 ${stocks.size}개의 종목을 로드했습니다.")
|
||||
stocks.forEach { (code, name) ->
|
||||
// println("📌 로드됨: [$code] $name")
|
||||
addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name))
|
||||
}
|
||||
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 && it.code !in executionCache.values.map { it.code } && it.code !in failList}
|
||||
.distinctBy { it.code })
|
||||
if (now.isAfter(H16)) {
|
||||
executeClosingLiquidation(KisTradeService)
|
||||
} else {
|
||||
println("미확인 데이터 ${remainingCandidates.size}")
|
||||
// remainingCandidates.removeIf { it.code in myHoldings || it.code in pendingStocks || it.code in executionCache.values.map { it.code } || it.code in failList}
|
||||
}
|
||||
|
||||
|
||||
var totalCount = remainingCandidates.size
|
||||
println("후보군 조건 충족 총 개수 : ${totalCount}")
|
||||
val iterator = remainingCandidates.iterator()
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
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()}")
|
||||
}
|
||||
}
|
||||
|
||||
//장외
|
||||
now.isAfter(H16) || now.isBefore(H08M50) -> {
|
||||
when {
|
||||
(now.hour == 0 && now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
|
||||
waitTime = 10.0
|
||||
isSystemReadyToday = false
|
||||
isSystemCleanedUpToday = false
|
||||
}
|
||||
|
||||
(now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
|
||||
waitTime = 3.0
|
||||
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||
KisWebSocketManager.disconnect()
|
||||
tryRefreshToken()
|
||||
}
|
||||
}
|
||||
|
||||
(now.isAfter(LocalTime.of(18, 0))) -> {
|
||||
try {
|
||||
waitTime = 5.0
|
||||
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
|
||||
if (!isSystemCleanedUpToday) {
|
||||
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
|
||||
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
|
||||
KisWebSocketManager.disconnect()
|
||||
BrowserManager.closeIfIdle(0) // 즉시 닫기
|
||||
if (LlamaServerManager.stopAll()) {
|
||||
isSystemCleanedUpToday = true
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
executeMarketLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// //장외
|
||||
// now.isAfter(H16) || now.isBefore(H08M35) -> {
|
||||
// finalizeMarketClose(now)
|
||||
// }
|
||||
else ->{
|
||||
waitTime = 3.0
|
||||
}
|
||||
@ -619,6 +494,150 @@ object AutoTradingManager {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun prepareMarketOpen(now : LocalTime) {
|
||||
if (now.isAfter(H16) || now.isBefore(H08M30)) {
|
||||
println("🌙 [System] 마감 시간 도달. 자원 정리 후 대기 모드(설정 화면)로 전환합니다.")
|
||||
onMarketClosed?.invoke()
|
||||
KisWebSocketManager.disconnect()
|
||||
BrowserManager.closeIfIdle(0)
|
||||
LlamaServerManager.stopAll() // AI 서버 완전 종료
|
||||
TradingLogStore.clear()
|
||||
// Main.kt에 설정 화면으로 가라고 신호 전송
|
||||
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
||||
} else if (now.isAfter(H08M30) && now.isBefore(H08M35) && !isSystemReadyToday) {
|
||||
if (MarketUtil.canTradeToday()) {
|
||||
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
|
||||
tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true)
|
||||
} else {
|
||||
println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.")
|
||||
isSystemReadyToday = false
|
||||
delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여
|
||||
}
|
||||
}
|
||||
}
|
||||
var loadedTops = mutableListOf<Pair<String, String>>()
|
||||
|
||||
fun poll100Stocks(): List<Pair<String, String>> {
|
||||
val count = minOf(loadedTops.size, 100)
|
||||
if (count == 0) return emptyList()
|
||||
|
||||
// 앞의 100개를 복사
|
||||
val batch = loadedTops.subList(0, count).toList()
|
||||
|
||||
// 원본에서 삭제 (이 작업이 큐의 pop/remove 역할을 합니다)
|
||||
loadedTops.subList(0, count).clear()
|
||||
|
||||
return batch
|
||||
}
|
||||
|
||||
suspend fun executeMarketLoop() {
|
||||
val balance = KisTradeService.fetchIntegratedBalance().getOrNull()
|
||||
balance?.let { resumePendingSellOrders(KisTradeService, 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 }
|
||||
if (remainingCandidates.isEmpty()) {
|
||||
if (loadedTops.size < 100) {
|
||||
loadedTops.addAll(StockUniverseLoader.loadUniverse())
|
||||
loadedTops.shuffle()
|
||||
println("✅ 총 ${loadedTops.size}개의 종목이 로드되있음.")
|
||||
}
|
||||
poll100Stocks().forEach { (code, name) ->
|
||||
addToReanalysis(RankingStock(mksc_shrn_iscd = code, hts_kor_isnm = name))
|
||||
}
|
||||
|
||||
val candidates: MutableList<RankingStock> = fetchCandidates(KisTradeService).apply {
|
||||
}.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)
|
||||
}
|
||||
reanalysisList.clear()
|
||||
remainingCandidates.addAll(candidates.filter { it.code !in myHoldings && it.code !in pendingStocks && it.code !in executionCache.values.map { it.code } && it.code !in failList}
|
||||
.distinctBy { it.code })
|
||||
} else {
|
||||
println("미확인 데이터 ${remainingCandidates.size}")
|
||||
}
|
||||
|
||||
var totalCount = remainingCandidates.size
|
||||
println("후보군 조건 충족 총 개수 : ${totalCount}")
|
||||
val iterator = remainingCandidates.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
totalCount--
|
||||
val stock = iterator.next()
|
||||
try {
|
||||
processSingleStock(stock, myCash, KisTradeService, globalCallback)
|
||||
} catch (e: Exception) {
|
||||
println("❌ 처리 중 오류 발생 (건너뜀): ${stock.name}")
|
||||
} finally {
|
||||
iterator.remove()
|
||||
}
|
||||
println("남은 후보군 개수 : ${totalCount}")
|
||||
delay(100)
|
||||
}
|
||||
println("⏱️ [Cycle End] ${LocalTime.now()}")
|
||||
}
|
||||
|
||||
suspend fun finalizeMarketClose(now: LocalTime) {
|
||||
when {
|
||||
(AutoTradingManager.now.hour == 0 && AutoTradingManager.now.minute == 0 && (isSystemReadyToday || isSystemCleanedUpToday)) -> {
|
||||
waitTime = 10.0
|
||||
isSystemReadyToday = false
|
||||
isSystemCleanedUpToday = false
|
||||
}
|
||||
|
||||
(AutoTradingManager.now.isAfter(LocalTime.of(8, 0)) && !isSystemReadyToday) -> {
|
||||
waitTime = 3.0
|
||||
if (!KisSession.isMarketTokenValid() || !KisSession.isTradeTokenValid()) {
|
||||
KisWebSocketManager.disconnect()
|
||||
tryRefreshToken()
|
||||
}
|
||||
}
|
||||
|
||||
(AutoTradingManager.now.isAfter(LocalTime.of(18, 0))) -> {
|
||||
try {
|
||||
waitTime = 5.0
|
||||
println("current SystemCleanedUpToday is $isSystemCleanedUpToday")
|
||||
if (!isSystemCleanedUpToday) {
|
||||
println("🌙 [System] 업무 종료 및 자원 정리 시작...")
|
||||
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
|
||||
KisWebSocketManager.disconnect()
|
||||
BrowserManager.closeIfIdle(0) // 즉시 닫기
|
||||
if (LlamaServerManager.stopAll()) {
|
||||
isSystemCleanedUpToday = true
|
||||
}
|
||||
|
||||
}
|
||||
println("✅ [System] 오늘의 모든 정리가 완료되었습니다.")
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
(AutoTradingManager.now.isAfter(LocalTime.of(18, 15)) && AutoTradingManager.now.minute % 15 == 0) -> {
|
||||
try {
|
||||
waitTime = 5.0
|
||||
SystemSleepPreventer.sleepDisplay() // 모니터 끄기
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitTime = 5.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToReanalysis(stock: RankingStock) {
|
||||
val count = retryCountMap.getOrDefault(stock.code, 0)
|
||||
if (count < 10) { // 최대 2회까지만 재시도하여 무한 루프 방지
|
||||
@ -862,14 +881,16 @@ data class InvestmentScores(
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return """
|
||||
ultraShort $ultraShort
|
||||
shortTerm $shortTerm
|
||||
midTerm $midTerm
|
||||
longTerm $longTerm
|
||||
AVG : ${avg()}
|
||||
ultraShort : $ultraShort
|
||||
shortTerm : $shortTerm
|
||||
midTerm : $midTerm
|
||||
longTerm : $longTerm
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun avg() = listOf(ultraShort, shortTerm, midTerm, longTerm).average()
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@ -96,7 +96,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
|
||||
break // 성공하면 루프 탈출
|
||||
}
|
||||
}
|
||||
delay(60_000 * 2) // 1분마다 시간 체크
|
||||
delay(30_000) // 1분마다 시간 체크
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user