2026-02-04 14:52:09 +09:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import java.util.concurrent.TimeUnit
|
2026-02-05 14:26:02 +09:00
|
|
|
import org.slf4j.LoggerFactory
|
|
|
|
|
import ch.qos.logback.classic.Level
|
|
|
|
|
import ch.qos.logback.classic.Logger
|
2026-03-27 10:59:59 +09:00
|
|
|
import java.awt.MouseInfo
|
|
|
|
|
import java.awt.Robot
|
|
|
|
|
import java.time.LocalTime
|
|
|
|
|
import java.util.concurrent.Executors
|
2026-02-04 14:52:09 +09:00
|
|
|
|
|
|
|
|
object SystemSleepPreventer {
|
|
|
|
|
private var process: Process? = null
|
2026-03-27 10:59:59 +09:00
|
|
|
|
|
|
|
|
private val robot = Robot()
|
|
|
|
|
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
|
|
|
|
|
|
|
|
// 작동 시간 설정
|
|
|
|
|
private val startTime = LocalTime.of(8, 30)
|
|
|
|
|
private val endTime = LocalTime.of(16, 0)
|
|
|
|
|
|
2026-03-13 10:41:10 +09:00
|
|
|
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
|
2026-02-04 14:52:09 +09:00
|
|
|
|
2026-03-13 10:41:10 +09:00
|
|
|
!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()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 14:52:09 +09:00
|
|
|
/**
|
|
|
|
|
* 맥의 절전 모드 및 디스플레이 취침을 방지하는 명령 실행
|
|
|
|
|
*/
|
|
|
|
|
fun start() {
|
2026-03-17 10:50:13 +09:00
|
|
|
val os = System.getProperty("os.name").lowercase()
|
|
|
|
|
val arch = System.getProperty("os.arch").lowercase()
|
|
|
|
|
val isWin = os.contains("win")
|
2026-02-05 14:26:02 +09:00
|
|
|
val root = LoggerFactory.getLogger("Exposed") as Logger
|
|
|
|
|
root.level = Level.ERROR
|
2026-03-17 10:50:13 +09:00
|
|
|
if (!isWin) {
|
|
|
|
|
checkAndRequestAccessibility()
|
|
|
|
|
}
|
2026-02-04 14:52:09 +09:00
|
|
|
|
2026-03-17 10:50:13 +09:00
|
|
|
if (process?.isAlive == true) return
|
|
|
|
|
if (!isWin) {
|
2026-03-27 10:59:59 +09:00
|
|
|
// try {
|
|
|
|
|
// // -i: 시스템 절전 방지, -d: 디스플레이 취침 방지, -m: 디스크 유휴 상태 방지
|
|
|
|
|
// val command = listOf("caffeinate", "-i", "-d", "-m")
|
|
|
|
|
// process = ProcessBuilder(command).start()
|
|
|
|
|
// println("☕ [System] caffeinate 실행됨: 앱이 켜져 있는 동안 절전 모드가 방지됩니다.")
|
|
|
|
|
// } catch (e: Exception) {
|
|
|
|
|
// println("⚠️ [System] caffeinate 실행 실패: ${e.message}")
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
start2()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun start2() {
|
|
|
|
|
println("🚀 화면 잠금 방지 프로그램이 시작되었습니다. (작동 시간: $startTime ~ $endTime)")
|
|
|
|
|
|
|
|
|
|
// 1분(60초)마다 체크
|
|
|
|
|
scheduler.scheduleAtFixedRate({
|
|
|
|
|
if (isWorkingTime()) {
|
|
|
|
|
moveMouseSlightly()
|
|
|
|
|
} else {
|
|
|
|
|
println("💤 현재는 휴식 시간입니다. (${LocalTime.now().withNano(0)})")
|
|
|
|
|
}
|
|
|
|
|
}, 0, 60 * 2, TimeUnit.SECONDS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun isWorkingTime(): Boolean {
|
|
|
|
|
val now = LocalTime.now()
|
|
|
|
|
// 시작 시간 이후 AND 종료 시간 이전인지 확인
|
|
|
|
|
return now.isAfter(startTime) && now.isBefore(endTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun moveMouseSlightly() {
|
|
|
|
|
try {
|
|
|
|
|
val pointer = MouseInfo.getPointerInfo().location
|
|
|
|
|
val x = pointer.x
|
|
|
|
|
val y = pointer.y
|
|
|
|
|
|
|
|
|
|
// 현재 위치에서 1픽셀씩 이동했다가 복귀 (사용자 방해 최소화)
|
|
|
|
|
robot.mouseMove(x + 1, y + 1)
|
|
|
|
|
robot.mouseMove(x, y)
|
|
|
|
|
|
|
|
|
|
println("🖱️ [${LocalTime.now().withNano(0)}] 마우스 신호를 보냈습니다.")
|
|
|
|
|
|
|
|
|
|
robot.keyPress(java.awt.event.KeyEvent.VK_SHIFT)
|
|
|
|
|
robot.keyRelease(java.awt.event.KeyEvent.VK_SHIFT)
|
|
|
|
|
println("🖱️ [${LocalTime.now().withNano(0)}] 키보드 신호를 보냈습니다.")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ 마우스 제어 실패: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val osName = System.getProperty("os.name").lowercase()
|
|
|
|
|
|
|
|
|
|
// 설정 시간
|
|
|
|
|
private val dimTime = LocalTime.of(16, 0) // 오후 4시 이후 최저 밝기
|
|
|
|
|
|
|
|
|
|
fun start3() {
|
|
|
|
|
scheduler.scheduleAtFixedRate({
|
|
|
|
|
val now = LocalTime.now()
|
|
|
|
|
|
|
|
|
|
// 16:00 이후라면 밝기를 낮춤
|
|
|
|
|
if (now.isAfter(dimTime) || now.isBefore(LocalTime.of(8, 30))) {
|
|
|
|
|
setBrightness(0)
|
|
|
|
|
} else {
|
|
|
|
|
setBrightness(80) // 업무 시간에는 다시 밝게 (80%)
|
|
|
|
|
}
|
|
|
|
|
}, 0, 10, TimeUnit.MINUTES) // 10분마다 체크
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun setBrightness(level: Int) {
|
|
|
|
|
try {
|
|
|
|
|
if (osName.contains("mac")) {
|
|
|
|
|
// macOS: AppleScript 사용 (0.0 ~ 1.0 사이 값)
|
|
|
|
|
// val macLevel = level / 100.0
|
|
|
|
|
// val command = arrayOf("osascript", "-e",
|
|
|
|
|
// "tell application \"Image Events\" to set darkness to $macLevel")
|
|
|
|
|
// // 참고: 최신 macOS에서는 'display services' 관련 스크립트가 더 정확할 수 있습니다.
|
|
|
|
|
// Runtime.getRuntime().exec(arrayOf("osascript", "-e",
|
|
|
|
|
// "tell application \"System Events\" to repeat $level times \n key code 107 \n end repeat")) // 밝기 감소 키 반복
|
|
|
|
|
val keyCode = if (level < 50) 145 else 144 // 145: 감소, 144: 증가
|
|
|
|
|
val action = if (level < 50) "어둡게" else "밝게"
|
|
|
|
|
|
|
|
|
|
// 32번 연타하면 어떤 상태에서든 최소/최대에 도달합니다.
|
|
|
|
|
val script = """
|
|
|
|
|
tell application "System Events"
|
|
|
|
|
repeat 32 times
|
|
|
|
|
key code $keyCode
|
|
|
|
|
end repeat
|
|
|
|
|
end tell
|
|
|
|
|
""".trimIndent()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val process = ProcessBuilder("osascript", "-e", script).start()
|
|
|
|
|
process.waitFor()
|
|
|
|
|
println("🍏 Mac 화면을 $action 설정했습니다. ${keyCode}")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ AppleScript 실행 실패: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
} else if (osName.contains("win")) {
|
|
|
|
|
// Windows: PowerShell 사용 (0 ~ 100 사이 값)
|
|
|
|
|
val psCommand = "Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods | ForEach-Object { \$.WmiSetBrightness(1, $level) }"
|
|
|
|
|
ProcessBuilder("powershell.exe", "-Command", psCommand).start()
|
2026-03-17 10:50:13 +09:00
|
|
|
}
|
2026-03-27 10:59:59 +09:00
|
|
|
println("🔆 밝기를 $level%로 설정했습니다.")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ 밝기 조절 실패: ${e.message}")
|
2026-02-04 14:52:09 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 10:41:10 +09:00
|
|
|
/**
|
|
|
|
|
* 모니터를 즉시 잠자기 모드로 전환
|
|
|
|
|
*/
|
|
|
|
|
fun sleepDisplay() {
|
|
|
|
|
try {
|
2026-03-27 10:59:59 +09:00
|
|
|
setBrightness(0)
|
2026-03-13 10:41:10 +09:00
|
|
|
println("🌙 [System] 오후 6시 30분: 모니터를 잠자기 모드로 전환합니다.")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ 모니터 잠자기 실패: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 10:59:59 +09:00
|
|
|
fun preventSleepWithRobot() {
|
|
|
|
|
try {
|
|
|
|
|
val robot = Robot()
|
|
|
|
|
// 현재 마우스 위치 가져오기
|
|
|
|
|
val pointer = MouseInfo.getPointerInfo().location
|
|
|
|
|
val x = pointer.x
|
|
|
|
|
val y = pointer.y
|
|
|
|
|
|
|
|
|
|
// 마우스를 1픽셀 옆으로 움직였다가 다시 제자리로 (OS는 활동으로 간주함)
|
|
|
|
|
robot.mouseMove(x + 1, y + 1)
|
|
|
|
|
robot.mouseMove(x, y)
|
|
|
|
|
|
|
|
|
|
println("🖱️ 마우스 이벤트를 시뮬레이션하여 화면 잠금을 방지했습니다.")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ 이벤트 생성 실패: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 10:41:10 +09:00
|
|
|
/**
|
|
|
|
|
* 마우스 움직임을 시뮬레이션하거나 디스플레이 깨우기 명령 실행
|
|
|
|
|
*/
|
|
|
|
|
fun wakeDisplay() {
|
|
|
|
|
try {
|
2026-03-27 10:59:59 +09:00
|
|
|
setBrightness(100)
|
2026-03-13 10:41:10 +09:00
|
|
|
println("☀️ 오전 8시: 모니터를 깨웁니다.")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
println("⚠️ 모니터 깨우기 실패: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:52:09 +09:00
|
|
|
/**
|
|
|
|
|
* 앱 종료 시 프로세스 함께 종료
|
|
|
|
|
*/
|
|
|
|
|
fun stop() {
|
|
|
|
|
process?.destroy()
|
|
|
|
|
// 프로세스가 강제 종료되지 않을 경우를 대비해 0.5초 대기 후 강제 종료
|
|
|
|
|
if (process?.waitFor(500, TimeUnit.MILLISECONDS) == false) {
|
|
|
|
|
process?.destroyForcibly()
|
|
|
|
|
}
|
|
|
|
|
println("🛑 [System] caffeinate 종료됨: 시스템 절전 설정이 정상화됩니다.")
|
|
|
|
|
}
|
|
|
|
|
}
|