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(); });