...
This commit is contained in:
parent
af0dc6b15f
commit
d6cfcbd579
@ -448,6 +448,21 @@ object TradingLogStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addAnalyzer(name : String, code : String, log: String) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
||||||
|
decisionLogs.add(
|
||||||
|
LogEntry(
|
||||||
|
time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),
|
||||||
|
stockName = "$name[$code] 분석",
|
||||||
|
decision = "ANALYZER",
|
||||||
|
confidence = 100.0,
|
||||||
|
reason = log
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addSettingLog(settingDesc : String, old : String, new : String, log: String) {
|
fun addSettingLog(settingDesc : String, old : String, new : String, log: String) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
if (decisionLogs.size > 1000) decisionLogs.removeAt(0)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import model.KisSession
|
import model.KisSession
|
||||||
import model.TokenRequest
|
import model.TokenRequest
|
||||||
import model.TokenResponse
|
import model.TokenResponse
|
||||||
|
import service.AutoTradingManager
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
object KisAuthService {
|
object KisAuthService {
|
||||||
@ -66,8 +67,10 @@ object KisAuthService {
|
|||||||
tradeToken = tData.access_token,
|
tradeToken = tData.access_token,
|
||||||
tradeTokenExpiredAt = LocalDateTime.now().plusSeconds(tData.expires_in),
|
tradeTokenExpiredAt = LocalDateTime.now().plusSeconds(tData.expires_in),
|
||||||
)
|
)
|
||||||
|
AutoTradingManager.tradeToken = true
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
AutoTradingManager.tradeToken = false
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonObject
|
|||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
import model.RealTimeTrade
|
import model.RealTimeTrade
|
||||||
|
import service.AutoTradingManager
|
||||||
import util.AesCrypto
|
import util.AesCrypto
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -90,6 +91,7 @@ object KisWebSocketManager {
|
|||||||
println("✅ 웹소켓 세션 진입 성공")
|
println("✅ 웹소켓 세션 진입 성공")
|
||||||
session = this
|
session = this
|
||||||
isConnected.set(true)
|
isConnected.set(true)
|
||||||
|
AutoTradingManager.webSocketConnect = true
|
||||||
println("✅ 웹소켓 연결 성공")
|
println("✅ 웹소켓 연결 성공")
|
||||||
|
|
||||||
// 기존 구독 신청 로직 (H0STCNI0 등)
|
// 기존 구독 신청 로직 (H0STCNI0 등)
|
||||||
@ -111,12 +113,14 @@ object KisWebSocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
AutoTradingManager.webSocketConnect = false
|
||||||
println("❌ 웹소켓 연결 끊김: ${e.message}")
|
println("❌ 웹소켓 연결 끊김: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
println("🏁 웹소켓 finally 블록 진입 (연결 시도 종료)")
|
println("🏁 웹소켓 finally 블록 진입 (연결 시도 종료)")
|
||||||
isConnected.set(false)
|
isConnected.set(false)
|
||||||
session = null
|
session = null
|
||||||
|
AutoTradingManager.webSocketConnect = false
|
||||||
println("⏳ 5초 후 재연결 시도...")
|
println("⏳ 5초 후 재연결 시도...")
|
||||||
delay(5000) // 5초 후 재연결 시도
|
delay(5000) // 5초 후 재연결 시도
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package network// src/main/kotlin/network/RagService.kt
|
package network// src/main/kotlin/network/RagService.kt
|
||||||
|
|
||||||
|
import TradingLogStore
|
||||||
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
|
import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
|
||||||
import dev.langchain4j.data.document.Metadata
|
import dev.langchain4j.data.document.Metadata
|
||||||
import dev.langchain4j.data.segment.TextSegment
|
import dev.langchain4j.data.segment.TextSegment
|
||||||
@ -214,13 +215,18 @@ object RagService {
|
|||||||
)
|
)
|
||||||
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
|
TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}")
|
||||||
|
println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}")
|
||||||
result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true)
|
result(decideTrading(stockCode, scores, financialStmt, tradingDecision), true)
|
||||||
} else {
|
} else {
|
||||||
println("${corpInfo?.cName} : ${scores.toString()}")
|
println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}")
|
||||||
|
TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}${scores.toString()}")
|
||||||
tradingDecision.confidence = 1.0
|
tradingDecision.confidence = 1.0
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
TradingLogStore.addAnalyzer(stockName,stockCode, "${FinancialAnalyzer.toString(financialStmt)}")
|
||||||
|
println("${stockName}[${stockCode}] : ${FinancialAnalyzer.toString(financialStmt)}")
|
||||||
tradingDecision.confidence = 1.0
|
tradingDecision.confidence = 1.0
|
||||||
result(tradingDecision, false)
|
result(tradingDecision, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import network.TradingDecision
|
|||||||
import TradingLogStore
|
import TradingLogStore
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import getLlamaBinPath
|
import getLlamaBinPath
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -35,6 +36,7 @@ import network.KisTradeService
|
|||||||
import network.KisWebSocketManager
|
import network.KisWebSocketManager
|
||||||
import network.RagService
|
import network.RagService
|
||||||
import network.StockUniverseLoader
|
import network.StockUniverseLoader
|
||||||
|
import org.jetbrains.skia.ImageFilter
|
||||||
import util.MarketUtil
|
import util.MarketUtil
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
@ -64,6 +66,12 @@ object AutoTradingManager {
|
|||||||
private val reanalysisList = mutableListOf<RankingStock>()
|
private val reanalysisList = mutableListOf<RankingStock>()
|
||||||
private val retryCountMap = mutableMapOf<String, Int>()
|
private val retryCountMap = mutableMapOf<String, Int>()
|
||||||
var shouldShowFullWindow by mutableStateOf(false)
|
var shouldShowFullWindow by mutableStateOf(false)
|
||||||
|
|
||||||
|
var llmAnalyser by mutableStateOf(false)
|
||||||
|
var llmNews by mutableStateOf(false)
|
||||||
|
var tradeToken by mutableStateOf(false)
|
||||||
|
var webSocketConnect by mutableStateOf(false)
|
||||||
|
|
||||||
fun startBackgroundScheduler() {
|
fun startBackgroundScheduler() {
|
||||||
// scope.launch {
|
// scope.launch {
|
||||||
// while (isActive) {
|
// while (isActive) {
|
||||||
@ -123,15 +131,15 @@ object AutoTradingManager {
|
|||||||
var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
var investmentGrade : InvestmentGrade = AutoTradingManager.getInvestmentGrade(completeTradingDecision,totalScore, completeTradingDecision.confidence)
|
||||||
|
|
||||||
val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
|
val finalMargin = baseProfit * KisSession.config.getValues(investmentGrade.profitGuide)
|
||||||
println("""
|
// println("""
|
||||||
사명 : ${completeTradingDecision.corpName}
|
// 사명 : ${completeTradingDecision.corpName}
|
||||||
신뢰도 : ${completeTradingDecision.confidence + append}
|
// 신뢰도 : ${completeTradingDecision.confidence + append}
|
||||||
단기성 : ${completeTradingDecision.shortPossible() + append}
|
// 단기성 : ${completeTradingDecision.shortPossible() + append}
|
||||||
수익성 : ${completeTradingDecision.profitPossible()+ append}
|
// 수익성 : ${completeTradingDecision.profitPossible()+ append}
|
||||||
안전성 : ${completeTradingDecision.safePossible()+ append}
|
// 안전성 : ${completeTradingDecision.safePossible()+ append}
|
||||||
${investmentGrade.displayName} : ${investmentGrade.description}
|
// ${investmentGrade.displayName} : ${investmentGrade.description}
|
||||||
총점 : ${totalScore}
|
// 총점 : ${totalScore}
|
||||||
""".trimIndent())
|
// """.trimIndent())
|
||||||
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
|
println("🚀 [매수 진행] 토탈 스코어: ${String.format("%.1f", totalScore)} -> 종목: ${completeTradingDecision.stockCode}")
|
||||||
|
|
||||||
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
// basePrice(현재가 혹은 지정가)를 기준으로 매수 가능 수량 산출 (최소 1주 보장)
|
||||||
@ -167,7 +175,10 @@ object AutoTradingManager {
|
|||||||
TradingLogStore.addLog(completeTradingDecision,"BUY","[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
TradingLogStore.addLog(completeTradingDecision,"BUY","[$stockCode] 매수 추천 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
resultCheck(completeTradingDecision)
|
resultCheck(completeTradingDecision)
|
||||||
}
|
}
|
||||||
"SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
"SELL" -> {
|
||||||
|
TradingLogStore.addLog(completeTradingDecision,"SELL","[$stockCode] 매도 추천 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
|
println("[$stockCode] 매도: ${completeTradingDecision?.reason}")
|
||||||
|
}
|
||||||
"HOLD" -> {
|
"HOLD" -> {
|
||||||
append = 0.0
|
append = 0.0
|
||||||
TradingLogStore.addLog(completeTradingDecision,"HOLD","[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
TradingLogStore.addLog(completeTradingDecision,"HOLD","[$stockCode] 관망 유지 resultCheck: ${completeTradingDecision?.reason}")
|
||||||
@ -830,6 +841,40 @@ object FinancialAnalyzer {
|
|||||||
return highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy
|
return highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toString(fs : FinancialStatement): String {
|
||||||
|
var buffer = StringBuffer()
|
||||||
|
val isDebtSafe = fs.debtRatio < 200.0 // 부채비율 200% 미만
|
||||||
|
val isLiquiditySafe = fs.quickRatio > 80.0 // 당좌비율 80% 이상
|
||||||
|
val isNotDeficit = fs.isNetIncomePositive // 당기순이익은 일단 흑자여야 함
|
||||||
|
if ((isDebtSafe && isLiquiditySafe && isNotDeficit) == false) {
|
||||||
|
if (isDebtSafe)buffer.appendLine( "부채비율 200% 이상")
|
||||||
|
if (isLiquiditySafe)buffer.appendLine( "당좌비율 80% 미만")
|
||||||
|
if (isNotDeficit)buffer.appendLine( "당기순이익 적자")
|
||||||
|
buffer.appendLine("최소 기준 미달")
|
||||||
|
} else {
|
||||||
|
buffer.appendLine("최소 기준 충족")
|
||||||
|
}
|
||||||
|
|
||||||
|
val highProfitability = fs.roe >= 10.0 // ROE 10% 이상
|
||||||
|
val strongGrowth = fs.netIncomeGrowth >= 15.0 // 이익 성장률 15% 이상
|
||||||
|
val verySafeDebt = fs.debtRatio <= 100.0 // 부채비율 100% 이하 (안전)
|
||||||
|
val goodLiquidity = fs.quickRatio >= 120.0 // 당좌비율 120% 이상 (여유)
|
||||||
|
val businessHealthy = fs.isOperatingProfitPositive // 본업(영업이익)이 흑자
|
||||||
|
|
||||||
|
if ((highProfitability && strongGrowth && verySafeDebt && goodLiquidity && businessHealthy) == false) {
|
||||||
|
if(!highProfitability) buffer.appendLine( "ROE 10% 미만")
|
||||||
|
if(!strongGrowth) buffer.appendLine( "이익 성장률 15% 미만")
|
||||||
|
if(!verySafeDebt) buffer.appendLine( "부채비율 100% 이상 (안전성 미달)")
|
||||||
|
if(!goodLiquidity) buffer.appendLine( "당좌비율 120% 이하 (여유 없음)")
|
||||||
|
if(!businessHealthy) buffer.appendLine( "본업(영업이익)이 적자")
|
||||||
|
buffer.appendLine("재무 건전성 및 성장성 미달")
|
||||||
|
} else {
|
||||||
|
buffer.appendLine("재무 건전성 및 성장성 충족")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString()
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 종합 상태 반환 (UI 또는 로그용)
|
* 종합 상태 반환 (UI 또는 로그용)
|
||||||
*/
|
*/
|
||||||
@ -881,11 +926,11 @@ data class InvestmentScores(
|
|||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return """
|
return """
|
||||||
AVG : ${avg()}
|
평점 : ${avg()}
|
||||||
ultraShort : $ultraShort
|
초단 : $ultraShort
|
||||||
shortTerm : $shortTerm
|
단기 : $shortTerm
|
||||||
midTerm : $midTerm
|
중기 : $midTerm
|
||||||
longTerm : $longTerm
|
장기 : $longTerm
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -106,6 +106,12 @@ object LlamaServerManager {
|
|||||||
// println("[Server $port] $line")
|
// println("[Server $port] $line")
|
||||||
if (line?.contains("server is listening") == true) {
|
if (line?.contains("server is listening") == true) {
|
||||||
println("🚀 AI 서버 준비 완료 (Port: $port)")
|
println("🚀 AI 서버 준비 완료 (Port: $port)")
|
||||||
|
if (port == 8080){
|
||||||
|
AutoTradingManager.llmAnalyser = true
|
||||||
|
}
|
||||||
|
if (port == 8081){
|
||||||
|
AutoTradingManager.llmNews = true
|
||||||
|
}
|
||||||
if (processes.size > 1) {
|
if (processes.size > 1) {
|
||||||
println("[Cache] ${processes.size}")
|
println("[Cache] ${processes.size}")
|
||||||
RagService.active()
|
RagService.active()
|
||||||
|
|||||||
@ -24,13 +24,31 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
|
import service.AutoTradingManager
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TradingDecisionLog() {
|
fun TradingDecisionLog() {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var selectedFilter by remember { mutableStateOf("전체") }
|
var selectedFilter by remember { mutableStateOf("전체") }
|
||||||
val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING")
|
val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING","ANALYZER")
|
||||||
|
var llmAnalyser by remember { mutableStateOf(AutoTradingManager.llmAnalyser) }
|
||||||
|
LaunchedEffect(AutoTradingManager.llmAnalyser) {
|
||||||
|
llmAnalyser = AutoTradingManager.llmAnalyser
|
||||||
|
}
|
||||||
|
var llmNews by remember { mutableStateOf(AutoTradingManager.llmNews) }
|
||||||
|
LaunchedEffect(AutoTradingManager.llmNews) {
|
||||||
|
llmNews = AutoTradingManager.llmNews
|
||||||
|
}
|
||||||
|
var tradeToken by remember { mutableStateOf(AutoTradingManager.tradeToken) }
|
||||||
|
LaunchedEffect(AutoTradingManager.tradeToken) {
|
||||||
|
tradeToken = AutoTradingManager.tradeToken
|
||||||
|
}
|
||||||
|
var webSocketConnect by remember { mutableStateOf(AutoTradingManager.webSocketConnect) }
|
||||||
|
LaunchedEffect(AutoTradingManager.webSocketConnect) {
|
||||||
|
webSocketConnect = AutoTradingManager.webSocketConnect
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// [핵심] 원본 로그에서 필터 조건에 맞는 리스트만 산출
|
// [핵심] 원본 로그에서 필터 조건에 맞는 리스트만 산출
|
||||||
val filteredLogs = TradingLogStore.decisionLogs.filter { log ->
|
val filteredLogs = TradingLogStore.decisionLogs.filter { log ->
|
||||||
@ -42,8 +60,17 @@ fun TradingDecisionLog() {
|
|||||||
|
|
||||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||||
Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||||
Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6)
|
|
||||||
|
|
||||||
|
Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
StatusIndicator("주식 분석기",llmAnalyser)
|
||||||
|
StatusIndicator("뉴스 처리기",llmNews)
|
||||||
|
StatusIndicator("한투 인증서",tradeToken)
|
||||||
|
StatusIndicator("실시간 감시",webSocketConnect)
|
||||||
|
}
|
||||||
// [추가] 상단 검색 및 필터 UI
|
// [추가] 상단 검색 및 필터 UI
|
||||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
// 1. 검색창
|
// 1. 검색창
|
||||||
@ -95,6 +122,7 @@ fun TradingDecisionLog() {
|
|||||||
"SETTING" -> Color(0xFFFFA500)
|
"SETTING" -> Color(0xFFFFA500)
|
||||||
"SELL" -> Color(0xFF800080)
|
"SELL" -> Color(0xFF800080)
|
||||||
"HOLD" -> Color.Gray
|
"HOLD" -> Color.Gray
|
||||||
|
"ANALYZER" -> Color.Green
|
||||||
else -> Color.Gray
|
else -> Color.Gray
|
||||||
},
|
},
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
@ -289,3 +317,28 @@ fun TradingDecisionLog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatusIndicator(label: String, isActive: Boolean, onRestart: (() -> Unit)? = null) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(end = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(text = label, style = MaterialTheme.typography.body2)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
// 상태 아이콘 (초록 O / 빨간 X)
|
||||||
|
Text(
|
||||||
|
text = if (isActive) "●" else "×",
|
||||||
|
color = if (isActive) Color.Green else Color.Red,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
// 문제가 있을 때(false)만 재구동 유도 버튼 표시
|
||||||
|
// if (!isActive && onRestart != null) {
|
||||||
|
// TextButton(onClick = onRestart, contentPadding = PaddingValues(0.dp)) {
|
||||||
|
// Text("재구동", color = Color.Blue, fontSize = 11.sp, textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user