2025-12-16 08:40:32 +09:00
|
|
|
import express from "express";
|
|
|
|
|
import { createServer } from "http";
|
|
|
|
|
import { Server } from "socket.io";
|
|
|
|
|
import path from "path";
|
|
|
|
|
import { fileURLToPath } from "url";
|
2025-12-22 09:36:23 +09:00
|
|
|
import session from "express-session";
|
2025-12-16 08:40:32 +09:00
|
|
|
|
|
|
|
|
// 모듈 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";
|
2025-12-22 09:36:23 +09:00
|
|
|
import authRoutes from "./routes/auth.js";
|
|
|
|
|
import linkRoutes from "./routes/link.js";
|
2025-12-22 15:37:54 +09:00
|
|
|
import adminRoutes from "./routes/admin.js";
|
2025-12-16 08:40:32 +09:00
|
|
|
|
|
|
|
|
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;
|
2025-12-22 09:36:23 +09:00
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-12-16 08:40:32 +09:00
|
|
|
|
|
|
|
|
// dist 디렉토리에서 정적 파일 제공
|
|
|
|
|
app.use(express.static(path.join(__dirname, "dist")));
|
|
|
|
|
|
|
|
|
|
// API 라우트
|
|
|
|
|
app.use("/api", apiRoutes);
|
|
|
|
|
|
2025-12-22 09:36:23 +09:00
|
|
|
// 인증 라우트
|
|
|
|
|
app.use("/auth", authRoutes);
|
|
|
|
|
|
|
|
|
|
// 마인크래프트 연동 라우트
|
|
|
|
|
app.use("/link", linkRoutes);
|
|
|
|
|
|
2025-12-22 15:37:54 +09:00
|
|
|
// 관리자 라우트
|
2025-12-23 10:07:34 +09:00
|
|
|
app.use("/api/admin", adminRoutes);
|
2025-12-22 15:37:54 +09:00
|
|
|
|
2025-12-16 08:40:32 +09:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 10:07:34 +09:00
|
|
|
// 로그 캐시 (중복 브로드캐스트 방지)
|
|
|
|
|
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) {
|
|
|
|
|
// 연결 오류 무시
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 08:40:32 +09:00
|
|
|
// 1초마다 데이터 갱신
|
|
|
|
|
setInterval(refreshAndBroadcast, 1000);
|
2025-12-23 10:07:34 +09:00
|
|
|
setInterval(refreshLogs, 1000);
|
2025-12-16 08:40:32 +09:00
|
|
|
refreshAndBroadcast();
|
2025-12-23 10:07:34 +09:00
|
|
|
refreshLogs();
|
2025-12-16 08:40:32 +09:00
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
});
|