feat: 콘솔 명령어 실행 API 구현
- 모드: POST /command 엔드포인트 추가 - 모드: minecraftServer 참조 및 서버 이벤트 핸들러 - 백엔드: admin.js 라우트 (JWT + 관리자 권한) - 프론트엔드: 실제 API 호출로 연동
This commit is contained in:
parent
67f9cac26f
commit
19143d969c
2 changed files with 92 additions and 2 deletions
|
|
@ -8,6 +8,7 @@ import co.caadiq.serverstatus.data.PlayerTracker
|
|||
import co.caadiq.serverstatus.data.ServerDataCollector
|
||||
import co.caadiq.serverstatus.data.WorldDataCollector
|
||||
import co.caadiq.serverstatus.network.HttpApiServer
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.neoforged.bus.api.IEventBus
|
||||
import net.neoforged.bus.api.SubscribeEvent
|
||||
import net.neoforged.fml.ModContainer
|
||||
|
|
@ -15,6 +16,8 @@ import net.neoforged.fml.common.Mod
|
|||
import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent
|
||||
import net.neoforged.neoforge.event.server.ServerStartedEvent
|
||||
import net.neoforged.neoforge.event.server.ServerStoppedEvent
|
||||
import org.apache.logging.log4j.LogManager
|
||||
|
||||
/** 메인 모드 클래스 서버 상태 정보를 HTTP API로 제공 */
|
||||
|
|
@ -37,6 +40,10 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
|
|||
private set
|
||||
lateinit var httpApiServer: HttpApiServer
|
||||
private set
|
||||
|
||||
// 마인크래프트 서버 인스턴스 (명령어 실행에 사용)
|
||||
var minecraftServer: MinecraftServer? = null
|
||||
private set
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -62,6 +69,20 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
|
|||
AuthCommand.register(event.dispatcher)
|
||||
}
|
||||
|
||||
/** 서버 시작 완료 이벤트 */
|
||||
@SubscribeEvent
|
||||
fun onServerStarted(event: ServerStartedEvent) {
|
||||
minecraftServer = event.server
|
||||
LOGGER.info("[$MOD_ID] 마인크래프트 서버 인스턴스 저장됨")
|
||||
}
|
||||
|
||||
/** 서버 종료 이벤트 */
|
||||
@SubscribeEvent
|
||||
fun onServerStopped(event: ServerStoppedEvent) {
|
||||
minecraftServer = null
|
||||
LOGGER.info("[$MOD_ID] 마인크래프트 서버 인스턴스 해제됨")
|
||||
}
|
||||
|
||||
/** 전용 서버 설정 이벤트 */
|
||||
private fun onServerSetup(event: FMLDedicatedServerSetupEvent) {
|
||||
LOGGER.info("[$MOD_ID] 서버 설정 중...")
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class HttpApiServer(private val port: Int) {
|
|||
server?.createContext("/players", PlayersHandler())
|
||||
server?.createContext("/player", PlayerHandler())
|
||||
server?.createContext("/worlds", WorldsHandler())
|
||||
server?.createContext("/command", CommandHandler())
|
||||
|
||||
server?.start()
|
||||
ServerStatusMod.LOGGER.info(
|
||||
|
|
@ -53,8 +54,8 @@ class HttpApiServer(private val port: Int) {
|
|||
private fun sendJsonResponse(exchange: HttpExchange, response: String, statusCode: Int = 200) {
|
||||
exchange.responseHeaders.add("Content-Type", "application/json; charset=utf-8")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
val bytes = response.toByteArray(StandardCharsets.UTF_8)
|
||||
exchange.sendResponseHeaders(statusCode, bytes.size.toLong())
|
||||
|
|
@ -206,6 +207,74 @@ class HttpApiServer(private val port: Int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /command - 서버 명령어 실행 */
|
||||
private inner class CommandHandler : HttpHandler {
|
||||
override fun handle(exchange: HttpExchange) {
|
||||
if (exchange.requestMethod == "OPTIONS") {
|
||||
sendJsonResponse(exchange, "", 204)
|
||||
return
|
||||
}
|
||||
|
||||
if (exchange.requestMethod != "POST") {
|
||||
sendJsonResponse(exchange, """{"error": "Method not allowed"}""", 405)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val body = exchange.requestBody.bufferedReader().readText()
|
||||
val request = json.decodeFromString<CommandRequest>(body)
|
||||
val command = request.command.trim()
|
||||
|
||||
if (command.isEmpty()) {
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"success": false, "message": "명령어가 비어있습니다"}""",
|
||||
400
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 메인 스레드에서 명령어 실행
|
||||
val server = ServerStatusMod.minecraftServer
|
||||
if (server == null) {
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"success": false, "message": "서버가 실행 중이 아닙니다"}""",
|
||||
503
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 슬래시 제거 (있으면)
|
||||
val cleanCommand = if (command.startsWith("/")) command.substring(1) else command
|
||||
|
||||
server.execute {
|
||||
try {
|
||||
val commandSource = server.createCommandSourceStack()
|
||||
server.commands.performPrefixedCommand(commandSource, cleanCommand)
|
||||
ServerStatusMod.LOGGER.info("[Admin] 명령어 실행: $cleanCommand")
|
||||
} catch (e: Exception) {
|
||||
ServerStatusMod.LOGGER.error("[Admin] 명령어 실행 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"success": true, "message": "명령어가 실행되었습니다: $cleanCommand"}"""
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ServerStatusMod.LOGGER.error("명령어 실행 오류: ${e.message}")
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"success": false, "message": "명령어 실행 중 오류 발생"}""",
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable data class WorldsResponse(val worlds: List<WorldInfo>)
|
||||
|
||||
@Serializable data class CommandRequest(val command: String)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue