...
This commit is contained in:
parent
7d03cc78b1
commit
a320dcde02
@ -766,7 +766,7 @@ object AutoTradingManager {
|
|||||||
lastForceCheckMinute = currentMinute // 실행 완료 기록
|
lastForceCheckMinute = currentMinute // 실행 완료 기록
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if((now.hour == 8 || now.hour == 16 || now.hour == 17) && (currentMinute % 10 == 1) || (currentMinute % 10 == 6)) {
|
else if((now.hour == 8 || now.hour == 16 || now.hour == 17 || now.hour == 18 || now.hour == 19) && (currentMinute % 10 == 1) || (currentMinute % 10 == 6)) {
|
||||||
if (lastForceCheckMinute != currentMinute) {
|
if (lastForceCheckMinute != currentMinute) {
|
||||||
TradingLogStore.addAnalyzer(
|
TradingLogStore.addAnalyzer(
|
||||||
" - ",
|
" - ",
|
||||||
@ -774,9 +774,9 @@ object AutoTradingManager {
|
|||||||
"⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
|
"⏰ [강제 스케줄 실행] 오후 ${now.hour}시 ${currentMinute}분 - 보유주식 시간외 단일가 또는 대체마켓 체크를 시작합니다.",
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
var list = mutableListOf<String>("x")
|
var list = mutableListOf<String>("X")
|
||||||
if (now.hour != 8) {
|
if (now.hour != 8) {
|
||||||
list.add("y")
|
list.add("Y")
|
||||||
}
|
}
|
||||||
list.forEach { code ->
|
list.forEach { code ->
|
||||||
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
|
KisTradeService.fetchIntegratedBalance(code).getOrNull()?.let {
|
||||||
|
|||||||
@ -205,55 +205,49 @@ fun TradingDecisionLog() {
|
|||||||
}
|
}
|
||||||
Column(modifier = Modifier.weight(0.5f).padding(6.dp).fillMaxHeight().background(Color.White)) {
|
Column(modifier = Modifier.weight(0.5f).padding(6.dp).fillMaxHeight().background(Color.White)) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(2), // 2열 병렬 배치
|
columns = GridCells.Fixed(6), // 💡 2와 3의 최소공배수인 6열로 통합!
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White)
|
modifier = Modifier.fillMaxSize().background(Color.White) // fillMaxWidth() 대신 전체 채우기
|
||||||
) {
|
) {
|
||||||
|
// ==========================================
|
||||||
|
// 1️⃣ 첫 번째 섹션: 2열 배치 구간 (span = 3)
|
||||||
|
// ==========================================
|
||||||
var firstSet = mutableSetOf<ConfigIndex>()
|
var firstSet = mutableSetOf<ConfigIndex>()
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지
|
||||||
Text(
|
Text(
|
||||||
"💰 거래 기본 설정",
|
"💰 거래 기본 설정",
|
||||||
style = MaterialTheme.typography.subtitle2,
|
style = MaterialTheme.typography.subtitle2,
|
||||||
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaults = arrayOf(
|
var defaults = arrayOf(
|
||||||
ConfigIndex.TAX_INDEX,
|
ConfigIndex.TAX_INDEX, ConfigIndex.PROFIT_INDEX, ConfigIndex.BUY_WEIGHT_INDEX,
|
||||||
ConfigIndex.PROFIT_INDEX,
|
ConfigIndex.MAX_BUDGET_INDEX, ConfigIndex.MAX_PRICE_INDEX, ConfigIndex.MIN_PRICE_INDEX,
|
||||||
ConfigIndex.BUY_WEIGHT_INDEX,
|
ConfigIndex.MIN_PURCHASE_SCORE_INDEX, ConfigIndex.SELL_PROFIT,
|
||||||
ConfigIndex.MAX_BUDGET_INDEX,
|
ConfigIndex.MAX_COUNT_INDEX, ConfigIndex.MAX_HOLDING_COUNT,
|
||||||
ConfigIndex.MAX_PRICE_INDEX,
|
|
||||||
ConfigIndex.MIN_PRICE_INDEX,
|
|
||||||
ConfigIndex.MIN_PURCHASE_SCORE_INDEX,
|
|
||||||
ConfigIndex.SELL_PROFIT,
|
|
||||||
ConfigIndex.MAX_COUNT_INDEX,
|
|
||||||
ConfigIndex.MAX_HOLDING_COUNT,
|
|
||||||
)
|
)
|
||||||
items(defaults.size) { index ->
|
|
||||||
|
// 💡 items에 span을 주어 3칸씩 차지하게 만듦 (결과적으로 2열 배치)
|
||||||
|
items(defaults.size, span = { GridItemSpan(3) }) { index ->
|
||||||
val configKey = defaults.get(index)
|
val configKey = defaults.get(index)
|
||||||
|
var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") }
|
||||||
|
|
||||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
|
||||||
var localText by remember(configKey) {
|
|
||||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 저장 로직을 공통 함수로 분리
|
|
||||||
val saveAction = {
|
val saveAction = {
|
||||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||||
var oldValue = KisSession.config.getValues(configKey)
|
var oldValue = KisSession.config.getValues(configKey)
|
||||||
if (configKey.label.contains("PROFIT")) {
|
if (configKey.label.contains("PROFIT")) {
|
||||||
newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
newValue = newValue / KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)
|
||||||
}
|
}
|
||||||
if (firstSet.contains(configKey)) {
|
if (firstSet.contains(configKey)) {
|
||||||
TradingLogStore.addSettingLog(configKey.label,oldValue.toString(),newValue.toString(),"💾 저장됨: ${configKey.label} = $newValue")
|
TradingLogStore.addSettingLog(configKey.label, oldValue.toString(), newValue.toString(), "💾 저장됨: ${configKey.label} = $newValue")
|
||||||
} else {
|
} else {
|
||||||
firstSet.add(configKey)
|
firstSet.add(configKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
KisSession.config.setValues(configKey, newValue)
|
KisSession.config.setValues(configKey, newValue)
|
||||||
DatabaseFactory.saveConfig(KisSession.config)
|
DatabaseFactory.saveConfig(KisSession.config)
|
||||||
println("💾 저장됨: ${configKey.label} = $newValue")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = if (configKey.label.contains("PROFIT")) {
|
var text = if (configKey.label.contains("PROFIT")) {
|
||||||
@ -264,55 +258,114 @@ fun TradingDecisionLog() {
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
onValueChange = { localText = it },
|
||||||
label = { Text(configKey.label) },
|
label = { Text(configKey.label) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState -> if (!focusState.isFocused) saveAction() },
|
||||||
// 2. 포커스를 잃었을 때 저장
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Decimal),
|
||||||
if (!focusState.isFocused) {
|
keyboardActions = KeyboardActions(onDone = { saveAction() }),
|
||||||
saveAction()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
keyboardType = KeyboardType.Decimal
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
|
||||||
onDone = {
|
|
||||||
saveAction()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// --- 🛡️ 수익 및 리스크 관리 섹션 ---
|
||||||
LazyVerticalGrid(
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
columns = GridCells.Fixed(3), // 2열 병렬 배치
|
Column {
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
Divider(color = Color.LightGray, thickness = 1.dp)
|
||||||
modifier = Modifier.fillMaxWidth().weight(0.5f).background(Color.White)
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
) {
|
Text(
|
||||||
var firstSet = mutableSetOf<ConfigIndex>()
|
text = "🛡️ 수익 및 리스크 관리",
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
style = MaterialTheme.typography.subtitle2,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
color = Color.DarkGray,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 2열 배치니까 각각 span = 3
|
||||||
|
item(span = { GridItemSpan(3) }) {
|
||||||
|
SettingSwitchField(
|
||||||
|
label = "자동 익절 활성화",
|
||||||
|
initialChecked = KisSession.config.getValues(ConfigIndex.TAKE_PROFIT) == 1.0,
|
||||||
|
onCheckedChange = { KisSession.config.setValues(ConfigIndex.TAKE_PROFIT, if (it) 1.0 else 0.0) },
|
||||||
|
helperText = "목표 수익률 도달 시 기계적 익절"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(span = { GridItemSpan(3) }) {
|
||||||
|
SettingSwitchField(
|
||||||
|
label = "자동 손절 활성화",
|
||||||
|
initialChecked = KisSession.config.getValues(ConfigIndex.STOP_LOSS) == 1.0,
|
||||||
|
onCheckedChange = { KisSession.config.setValues(ConfigIndex.STOP_LOSS, if (it) 1.0 else 0.0) },
|
||||||
|
helperText = "손실 방어선 도달 시 기계적 손절"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(span = { GridItemSpan(3) }) {
|
||||||
|
SettingInputField(
|
||||||
|
label = "최소 손절 라인 (%)",
|
||||||
|
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MINRATE).toString(),
|
||||||
|
placeholder = "-1.5",
|
||||||
|
onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MINRATE, it.toDoubleOrNull() ?: -1.5) },
|
||||||
|
helperText = "타협 가능한 최소 라인"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(span = { GridItemSpan(3) }) {
|
||||||
|
SettingInputField(
|
||||||
|
label = "최대 허용 손절률 (%)",
|
||||||
|
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAXRATE).toString(),
|
||||||
|
placeholder = "-5.0",
|
||||||
|
onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAXRATE, it.toDoubleOrNull() ?: -5.0) },
|
||||||
|
helperText = "절대 방어선 (기계적 매도)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 금액은 길게 뻗어야 하니 maxLineSpan
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
SettingInputField(
|
||||||
|
label = "최대 허용 손실 금액 (원)",
|
||||||
|
initialValue = KisSession.config.getValues(ConfigIndex.LOSS_MAX_MONEY).toLong().toString(),
|
||||||
|
placeholder = "50000",
|
||||||
|
onSave = { KisSession.config.setValues(ConfigIndex.LOSS_MAX_MONEY, it.toDoubleOrNull() ?: 0.0) },
|
||||||
|
helperText = "1종목당 허용할 수 있는 최대 손실액 (원 단위)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Divider(color = Color.LightGray, thickness = 1.dp)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2️⃣ 두 번째 섹션: 3열 배치 구간 (span = 2)
|
||||||
|
// ==========================================
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) { // 6칸 모두 차지
|
||||||
Text(
|
Text(
|
||||||
"💰매수 정책 및 기대 수익률",
|
"💰 매수 정책 및 기대 수익률",
|
||||||
style = MaterialTheme.typography.subtitle2,
|
style = MaterialTheme.typography.subtitle2,
|
||||||
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
modifier = Modifier.padding(top = 10.dp, bottom = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaults2 = arrayOf(
|
var defaults2 = arrayOf(
|
||||||
arrayOf(ConfigIndex.GRADE_5_BUY, ConfigIndex.GRADE_5_PROFIT,ConfigIndex.GRADE_5_ALLOCATIONRATE),
|
arrayOf(ConfigIndex.GRADE_5_BUY, ConfigIndex.GRADE_5_PROFIT, ConfigIndex.GRADE_5_ALLOCATIONRATE),
|
||||||
arrayOf(ConfigIndex.GRADE_4_BUY, ConfigIndex.GRADE_4_PROFIT,ConfigIndex.GRADE_4_ALLOCATIONRATE),
|
arrayOf(ConfigIndex.GRADE_4_BUY, ConfigIndex.GRADE_4_PROFIT, ConfigIndex.GRADE_4_ALLOCATIONRATE),
|
||||||
arrayOf(ConfigIndex.GRADE_3_BUY, ConfigIndex.GRADE_3_PROFIT,ConfigIndex.GRADE_3_ALLOCATIONRATE),
|
arrayOf(ConfigIndex.GRADE_3_BUY, ConfigIndex.GRADE_3_PROFIT, ConfigIndex.GRADE_3_ALLOCATIONRATE),
|
||||||
arrayOf(ConfigIndex.GRADE_2_BUY, ConfigIndex.GRADE_2_PROFIT,ConfigIndex.GRADE_2_ALLOCATIONRATE),
|
arrayOf(ConfigIndex.GRADE_2_BUY, ConfigIndex.GRADE_2_PROFIT, ConfigIndex.GRADE_2_ALLOCATIONRATE),
|
||||||
arrayOf(ConfigIndex.GRADE_1_BUY, ConfigIndex.GRADE_1_PROFIT,ConfigIndex.GRADE_1_ALLOCATIONRATE),
|
arrayOf(ConfigIndex.GRADE_1_BUY, ConfigIndex.GRADE_1_PROFIT, ConfigIndex.GRADE_1_ALLOCATIONRATE),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (items in defaults2) {
|
for (items in defaults2) {
|
||||||
val common = findLongestCommonSubstring(items.first().label,items.last().label)
|
val common = findLongestCommonSubstring(items.first().label, items.last().label)
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) { // 2열을 모두 차지함
|
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
Text(
|
Text(
|
||||||
common,
|
common,
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
@ -320,72 +373,42 @@ fun TradingDecisionLog() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items(items.size) { index ->
|
// 💡 items에 span을 주어 2칸씩 차지하게 만듦 (결과적으로 3열 배치)
|
||||||
|
items(items.size, span = { GridItemSpan(2) }) { index ->
|
||||||
val configKey = items.get(index)
|
val configKey = items.get(index)
|
||||||
|
var localText by remember(configKey) { mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "") }
|
||||||
// 1. 키보드 입력을 실시간으로 보여줄 로컬 상태 (String)
|
var labelText by remember(configKey) { mutableStateOf("") }
|
||||||
var localText by remember(configKey) {
|
|
||||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelText by remember(configKey) {
|
|
||||||
mutableStateOf(KisSession.config.getValues(configKey)?.toString() ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
val saveAction = {
|
val saveAction = {
|
||||||
var oldValue = KisSession.config.getValues(configKey)
|
var oldValue = KisSession.config.getValues(configKey)
|
||||||
var newValue = localText.toDoubleOrNull() ?: 0.0
|
var newValue = localText.toDoubleOrNull() ?: 0.0
|
||||||
//
|
|
||||||
KisSession.config.setValues(configKey, newValue)
|
KisSession.config.setValues(configKey, newValue)
|
||||||
DatabaseFactory.saveConfig(KisSession.config)
|
DatabaseFactory.saveConfig(KisSession.config)
|
||||||
if (firstSet.contains(configKey)) {
|
if (firstSet.contains(configKey)) {
|
||||||
TradingLogStore.addSettingLog(configKey.label,oldValue.toString(),newValue.toString(),"💾 저장됨: ${configKey.label} = $newValue")
|
TradingLogStore.addSettingLog(configKey.label, oldValue.toString(), newValue.toString(), "💾 저장됨: ${configKey.label} = $newValue")
|
||||||
} else {
|
} else {
|
||||||
firstSet.add(configKey)
|
firstSet.add(configKey)
|
||||||
}
|
}
|
||||||
labelText = if (configKey.name.contains("PROFIT")) {
|
|
||||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
|
||||||
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
|
||||||
ConfigIndex.TAX_INDEX)}"
|
|
||||||
} else if (configKey.name.contains("ALLOCATIONRATE")) {
|
|
||||||
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * newValue}원 투자}"
|
|
||||||
} else {
|
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// labelText 업데이트 로직 (기존과 동일)
|
||||||
labelText = if (configKey.name.contains("PROFIT")) {
|
labelText = if (configKey.name.contains("PROFIT")) {
|
||||||
getRemaining(configKey.label,common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 성향별 비율(${KisSession.config.getValues(configKey)}) + 세금제비용(${KisSession.config.getValues(
|
getRemaining(configKey.label, common) + ": 기준율(${KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)}) * 비율(${KisSession.config.getValues(configKey)}) + 세금(${KisSession.config.getValues(ConfigIndex.TAX_INDEX)}) = ${(localText.toDoubleOrNull() ?: 0.0) * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX) + KisSession.config.getValues(ConfigIndex.TAX_INDEX)}"
|
||||||
ConfigIndex.TAX_INDEX)}) = ${(localText.toDouble() * KisSession.config.getValues(ConfigIndex.PROFIT_INDEX)) + KisSession.config.getValues(
|
} else if (configKey.name.contains("ALLOCATIONRATE")) {
|
||||||
ConfigIndex.TAX_INDEX)} "
|
getRemaining(configKey.label, common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * (localText.toDoubleOrNull() ?: 0.0)}원"
|
||||||
} else if (configKey.name.contains("ALLOCATIONRATE")) {
|
|
||||||
getRemaining(configKey.label,common) + ": 최대 ${KisSession.config.getValues(ConfigIndex.MAX_BUDGET_INDEX) * localText.toDouble() }원 투자}"
|
|
||||||
} else {
|
} else {
|
||||||
getRemaining(configKey.label,common) + ": -${localText} 호가 매수}"
|
getRemaining(configKey.label, common) + ": -${localText} 호가 매수"
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = localText,
|
value = localText,
|
||||||
onValueChange = { localText = it }, // 화면에는 즉시 반영
|
onValueChange = { localText = it },
|
||||||
label = { Text(labelText) },
|
label = { Text(labelText) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState -> if (!focusState.isFocused) saveAction() },
|
||||||
// 2. 포커스를 잃었을 때 저장
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Decimal),
|
||||||
if (!focusState.isFocused) {
|
keyboardActions = KeyboardActions(onDone = { saveAction() }),
|
||||||
saveAction()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
keyboardType = KeyboardType.Decimal
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
// 3. 엔터(Done) 키를 눌렀을 때 저장
|
|
||||||
onDone = {
|
|
||||||
saveAction()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -533,3 +556,99 @@ fun CsvDropZone(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingInputField(
|
||||||
|
label: String,
|
||||||
|
initialValue: String, // 💡 value -> initialValue 로 변경
|
||||||
|
placeholder: String = "",
|
||||||
|
helperText: String = "",
|
||||||
|
onSave: (String) -> Unit // 💡 타자 칠 때마다가 아니라, 완료 시 저장하도록 콜백 변경
|
||||||
|
) {
|
||||||
|
// 💡 화면에 즉시 글자를 그려주기 위한 로컬 상태 (핵심 해결책)
|
||||||
|
var localText by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = localText,
|
||||||
|
onValueChange = { localText = it }, // 타자 칠 때 화면 즉시 반영
|
||||||
|
label = { Text(label, fontWeight = FontWeight.Bold) },
|
||||||
|
placeholder = { Text(placeholder) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
// 💡 포커스를 잃었을 때 (다른 칸을 클릭했을 때) 저장
|
||||||
|
if (!focusState.isFocused) {
|
||||||
|
onSave(localText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
keyboardType = KeyboardType.Decimal
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
// 💡 모바일 키보드나 키보드에서 엔터(Done) 쳤을 때 저장
|
||||||
|
onDone = {
|
||||||
|
onSave(localText)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (helperText.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = helperText,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingSwitchField(
|
||||||
|
label: String,
|
||||||
|
initialChecked: Boolean,
|
||||||
|
helperText: String = "",
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
// 💡 스위치 애니메이션을 즉각 보여주기 위한 로컬 상태
|
||||||
|
var localChecked by remember { mutableStateOf(initialChecked) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = localChecked,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
localChecked = isChecked // 화면 스위치 즉시 변경
|
||||||
|
onCheckedChange(isChecked) // 실제 DB/설정 저장 트리거
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helperText.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = helperText,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.padding(start = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user