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 } 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 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); } // 로그 캐시 (중복 브로드캐스트 방지) 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) { // 연결 오류 무시 } } // 1초마다 데이터 갱신 setInterval(refreshAndBroadcast, 1000); setInterval(refreshLogs, 1000); refreshAndBroadcast(); refreshLogs(); // 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(); });