diff --git a/src/main/kotlin/ui/SettingsScreen.kt b/src/main/kotlin/ui/SettingsScreen.kt index 83ea3ed..692bd0b 100644 --- a/src/main/kotlin/ui/SettingsScreen.kt +++ b/src/main/kotlin/ui/SettingsScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.onExternalDrag import androidx.compose.ui.text.font.FontWeight @@ -80,6 +81,9 @@ fun getPastedPathFromClipboard(): String? { @OptIn(ExperimentalComposeUiApi::class) @Composable fun SettingsScreen(onAuthSuccess: () -> Unit) { + + var isModelBoxFocused by remember { mutableStateOf(false) } + var isEmbedBoxFocused by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() var config by remember { mutableStateOf(KisSession.config) } var statusMessage by remember { mutableStateOf("프로그램 초기화") } @@ -129,14 +133,14 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { while (true) { val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul")) // 08:30 ~ 15:30 사이이고, 키값이 최소한 하나라도 존재할 때 자동 실행 - if (now.isAfter(java.time.LocalTime.of(7, 40)) && now.isBefore(java.time.LocalTime.of(18, 0))) { - if (config.realAppKey.isNotEmpty() && config.vtsAppKey.isNotEmpty() && config.embedModelPath.isNotEmpty() && config.modelPath.isNotEmpty()) { - SystemSleepPreventer.wakeDisplay() // 모니터 켜기 - statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다." - authenticateAndStart() - break // 성공하면 루프 탈출 - } - } +// if (now.isAfter(java.time.LocalTime.of(7, 40)) && now.isBefore(java.time.LocalTime.of(18, 0))) { +// if (config.realAppKey.isNotEmpty() && config.vtsAppKey.isNotEmpty() && config.embedModelPath.isNotEmpty() && config.modelPath.isNotEmpty()) { +// SystemSleepPreventer.wakeDisplay() // 모니터 켜기 +// statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다." +// authenticateAndStart() +// break // 성공하면 루프 탈출 +// } +// } delay(30_000) // 1분마다 시간 체크 } } @@ -228,18 +232,46 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { ) { Box( modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) + .border( + width = if (isModelBoxFocused) 2.dp else 1.dp, + color = if (isModelBoxFocused) Color.Blue else Color.Gray, + shape = RoundedCornerShape(8.dp) + ) + // 2. Modifier 순서가 매우 중요합니다. .focusRequester(modelFocusRequester) - // 2. 포커스 가능하게 설정 + .onFocusChanged { focusState -> + isModelBoxFocused = focusState.isFocused // 포커스 여부 저장 + if (focusState.isFocused) { + val rawUri = getPastedPathFromClipboard() + println("rawUri $rawUri") + if (rawUri != null && rawUri.endsWith(".gguf", ignoreCase = true)) { + var path = rawUri.removePrefix("file://").removePrefix("file:") + + // 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거 + if (path.startsWith("/") && path.getOrNull(2) == ':') { + path = path.drop(1) + } + + if (path.endsWith(".gguf")) config = config.copy(modelPath = path) + true // 이벤트 소비 처리 + } + println("✅ Box에 포커스가 들어왔습니다!") // 로그 확인 + } + } .focusable() - // 3. 클릭하면 해당 박스로 키보드 포커스 이동 - .clickable { modelFocusRequester.requestFocus() } - // 4. 키보드 이벤트 (Ctrl+V / Cmd+V) 감지 - .onKeyEvent { event -> - // 키를 떼는 순간(KeyUp) && 누른 키가 'V' && (Ctrl 혹은 Cmd 키가 눌린 상태) + .clickable { + modelFocusRequester.requestFocus() + } + // 3. onKeyEvent 대신 onPreviewKeyEvent 사용! (핵심) + .onPreviewKeyEvent { event -> + // 디버깅용 로그: 어떤 키가 눌리는지 확인 + println("키 입력 감지: ${event.key}, type: ${event.type}") + if (event.type == KeyEventType.KeyUp && event.key == Key.V && (event.isCtrlPressed || event.isMetaPressed) ) { + println("🚀 Ctrl+V 입력 성공!") val rawUri = getPastedPathFromClipboard() if (rawUri != null && rawUri.endsWith(".gguf", ignoreCase = true)) { var path = rawUri.removePrefix("file://").removePrefix("file:") @@ -250,7 +282,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { } if (path.endsWith(".gguf")) config = config.copy(modelPath = path) - return@onKeyEvent true // 이벤트 소비 처리 + true // 이벤트 소비 처리 } } false @@ -275,26 +307,55 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) { contentAlignment = Alignment.Center ) { Text( - if (config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, - fontSize = 12.sp + if (config.modelPath.isEmpty()) "클릭하여 파란 테두리가 생기면\nCtrl+V 로 붙여넣기 하세요" + else config.modelPath, + fontSize = 12.sp, + textAlign = androidx.compose.ui.text.style.TextAlign.Center ) } Box( modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) + .border( + width = if (isEmbedBoxFocused) 2.dp else 1.dp, + color = if (isEmbedBoxFocused) Color.Blue else Color.Gray, + shape = RoundedCornerShape(8.dp) + ) + // 2. Modifier 순서가 매우 중요합니다. .focusRequester(embedModelFocusRequester) - // 2. 포커스 가능하게 설정 + .onFocusChanged { focusState -> + isEmbedBoxFocused = focusState.isFocused // 포커스 여부 저장 + if (focusState.isFocused) { + val rawUri = getPastedPathFromClipboard() + println("rawUri $rawUri") + if (rawUri != null && rawUri.endsWith(".gguf", ignoreCase = true)) { + var path = rawUri.removePrefix("file://").removePrefix("file:") + + // 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거 + if (path.startsWith("/") && path.getOrNull(2) == ':') { + path = path.drop(1) + } + + if (path.endsWith(".gguf")) config = config.copy(modelPath = path) + true // 이벤트 소비 처리 + } + println("✅ Box에 포커스가 들어왔습니다!") // 로그 확인 + } + } .focusable() - // 3. 클릭하면 해당 박스로 키보드 포커스 이동 - .clickable { embedModelFocusRequester.requestFocus() } + .clickable { + embedModelFocusRequester.requestFocus() + } // 4. 키보드 이벤트 (Ctrl+V / Cmd+V) 감지 .onKeyEvent { event -> + println("event >> $event") // 키를 떼는 순간(KeyUp) && 누른 키가 'V' && (Ctrl 혹은 Cmd 키가 눌린 상태) if (event.type == KeyEventType.KeyUp && event.key == Key.V && (event.isCtrlPressed || event.isMetaPressed) ) { val rawUri = getPastedPathFromClipboard() + println("rawUri >> $rawUri") if (rawUri != null && rawUri.endsWith(".gguf", ignoreCase = true)) { var path = rawUri.removePrefix("file://").removePrefix("file:")