minecraft-web/backend/server.js
caadiq fe86d02a72 feat: 소켓으로 마인크래프트 서버 상태 실시간 감지
- 백엔드: 3초마다 서버 상태 확인, 변경 시 브로드캐스트
- 프론트엔드: minecraft_servers 이벤트로 UI 자동 업데이트
- 외부에서 서버 시작/종료해도 UI에 반영
2025-12-29 17:46:07 +09:00

218 lines
5.8 KiB
JavaScript

import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import path from "path";
import { fileURLToPath } from "url";
import session from "express-session";
// 모듈 import
import { loadTranslations, initModpacksTable } from "./lib/db.js";
import {
MOD_API_URL,
refreshData,
fetchPlayerDetail,
getCachedStatus,
getCachedPlayers,
} from "./lib/minecraft.js";
import apiRoutes from "./routes/api.js";
import authRoutes from "./routes/auth.js";
import linkRoutes from "./routes/link.js";
import adminRoutes, {
getServerList,
getRunningServer,
getContainerStatus,
} from "./routes/admin.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
const PORT = process.env.PORT || 80;
const SESSION_SECRET = process.env.SESSION_SECRET || "minecraft-status-secret";
// JSON 파싱 미들웨어
app.use(express.json());
// 프록시 신뢰 (X-Forwarded-For 헤더 사용)
app.set("trust proxy", 1);
// 세션 설정
app.use(
session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // HTTPS가 아닌 환경에서도 작동 (프록시 뒤에서는 Caddy가 HTTPS 처리)
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24시간
sameSite: "lax",
},
})
);
// dist 디렉토리에서 정적 파일 제공
app.use(express.static(path.join(__dirname, "dist")));
// API 라우트
app.use("/api", apiRoutes);
// 인증 라우트
app.use("/auth", authRoutes);
// 마인크래프트 연동 라우트
app.use("/link", linkRoutes);
// 관리자 라우트
app.use("/api/admin", adminRoutes);
// Socket.IO 연결 처리
io.on("connection", (socket) => {
console.log("클라이언트 연결됨:", socket.id);
// 연결 시 즉시 캐시된 데이터 전송
const cachedStatus = getCachedStatus();
const cachedPlayers = getCachedPlayers();
if (cachedStatus) socket.emit("status", cachedStatus);
if (cachedPlayers.length > 0) socket.emit("players", cachedPlayers);
// 특정 플레이어 정보 요청 처리
socket.on("get_player", async (uuid) => {
const player = await fetchPlayerDetail(uuid);
if (player) socket.emit("player_detail", player);
});
// 월드 정보 요청 처리
socket.on("get_worlds", async () => {
try {
const response = await fetch(`${MOD_API_URL}/worlds`);
if (response.ok) {
const data = await response.json();
socket.emit("worlds", data);
}
} catch (error) {
console.error("[ModAPI] 월드 조회 실패:", error.message);
}
});
// 플레이어 통계 요청 처리
socket.on("get_player_stats", async (uuid) => {
try {
const response = await fetch(`${MOD_API_URL}/player/${uuid}/stats`);
if (response.ok) {
const data = await response.json();
socket.emit("player_stats", data);
}
} catch (error) {
console.error("[ModAPI] 통계 조회 실패:", error.message);
}
});
socket.on("disconnect", () => {
console.log("클라이언트 연결 해제:", socket.id);
});
});
// 주기적 데이터 갱신 및 브로드캐스트
async function refreshAndBroadcast() {
const { cachedStatus, cachedPlayers } = await refreshData();
io.emit("status", cachedStatus);
io.emit("players", cachedPlayers);
// 월드 정보도 브로드캐스트 (시간/날씨 포함)
try {
const response = await fetch(`${MOD_API_URL}/worlds`);
if (response.ok) {
const data = await response.json();
io.emit("worlds", data);
}
} catch (error) {
// 연결 오류 무시
}
}
// 로그 캐시 (중복 브로드캐스트 방지)
let lastLogCount = 0;
// 로그 갱신 및 브로드캐스트
async function refreshLogs() {
try {
const response = await fetch(`${MOD_API_URL}/logs`);
const data = await response.json();
if (data.logs && data.logs.length !== lastLogCount) {
lastLogCount = data.logs.length;
io.emit("logs", data.logs);
}
} catch (error) {
// 연결 오류 무시
}
}
// 마인크래프트 서버 상태 캐시 (변경 감지용)
let lastServerStatusHash = "";
// 마인크래프트 서버 상태 갱신 및 브로드캐스트
async function refreshServerStatus() {
try {
const servers = await getServerList();
const runningContainer = await getRunningServer();
// 각 서버의 상태 확인
const serversWithStatus = await Promise.all(
servers.map(async (server) => {
const status = await getContainerStatus(server.path);
return {
...server,
running: status === "running",
status,
};
})
);
// 상태 변경 감지 (해시 비교)
const currentHash = JSON.stringify({ serversWithStatus, runningContainer });
if (currentHash !== lastServerStatusHash) {
lastServerStatusHash = currentHash;
io.emit("minecraft_servers", {
servers: serversWithStatus,
runningContainer,
});
}
} catch (error) {
// 오류 무시 (서버 디렉토리가 없을 수 있음)
}
}
// 1초마다 데이터 갱신
setInterval(refreshAndBroadcast, 1000);
setInterval(refreshLogs, 1000);
setInterval(refreshServerStatus, 3000); // 서버 상태는 3초마다
refreshAndBroadcast();
refreshLogs();
refreshServerStatus();
// SPA 라우팅 - 모든 경로에 대해 index.html 제공
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html"));
});
// 서버 시작
httpServer.listen(PORT, async () => {
console.log(`[Server] 웹 서버 시작: 포트 ${PORT}`);
console.log(`[Server] 모드 API URL: ${MOD_API_URL}`);
// 번역 데이터 로드
await loadTranslations();
// 모드팩 테이블 초기화
await initModpacksTable();
});