diff --git a/backend/package.json b/backend/package.json index 9686e77..e3faf55 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.400.0", + "adm-zip": "^0.5.16", "bcryptjs": "^2.4.3", "express": "^4.18.2", "express-session": "^1.17.3", diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 69d263e..6f8ee3a 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -202,13 +202,11 @@ router.get("/whitelist", async (req, res) => { res.json(data); } catch (error) { console.error("[Admin] 화이트리스트 조회 오류:", error); - res - .status(500) - .json({ - enabled: false, - players: [], - error: "서버에 연결할 수 없습니다", - }); + res.status(500).json({ + enabled: false, + players: [], + error: "서버에 연결할 수 없습니다", + }); } }); @@ -345,4 +343,134 @@ function formatFileSize(bytes) { return bytes + " B"; } +/** + * POST /api/admin/modpacks - 모드팩 업로드 + * multipart/form-data: file (.mrpack), changelog (옵션) + */ +router.post( + "/modpacks", + express.raw({ type: "multipart/form-data", limit: "500mb" }), + async (req, res) => { + try { + const boundary = req.headers["content-type"]?.split("boundary=")[1]; + if (!boundary) { + return res.status(400).json({ error: "Invalid content-type" }); + } + + const body = req.body.toString("binary"); + const parts = body.split(`--${boundary}`); + + let fileData = null; + let fileName = ""; + let changelog = ""; + + // multipart 파싱 + for (const part of parts) { + if (part.includes('name="file"')) { + const match = part.match(/filename="([^"]+)"/); + if (match) fileName = match[1]; + const dataStart = part.indexOf("\r\n\r\n") + 4; + const dataEnd = part.lastIndexOf("\r\n"); + fileData = Buffer.from(part.slice(dataStart, dataEnd), "binary"); + } else if (part.includes('name="changelog"')) { + const dataStart = part.indexOf("\r\n\r\n") + 4; + const dataEnd = part.lastIndexOf("\r\n"); + changelog = part.slice(dataStart, dataEnd); + } + } + + if (!fileData || !fileName.endsWith(".mrpack")) { + return res.status(400).json({ error: ".mrpack 파일이 필요합니다" }); + } + + // .mrpack은 ZIP 파일. modrinth.index.json 파싱 + const { unzipSync } = await import("zlib"); + const AdmZip = (await import("adm-zip")).default; + const zip = new AdmZip(fileData); + const indexEntry = zip.getEntry("modrinth.index.json"); + + if (!indexEntry) { + return res + .status(400) + .json({ error: "modrinth.index.json을 찾을 수 없습니다" }); + } + + const indexJson = JSON.parse(indexEntry.getData().toString("utf8")); + const modpackName = indexJson.name || "Unknown Modpack"; + const modpackVersion = indexJson.versionId || "1.0.0"; + const minecraftVersion = indexJson.dependencies?.minecraft || null; + const modLoader = + Object.keys(indexJson.dependencies || {}).find( + (k) => k !== "minecraft" + ) || null; + + // contents 파싱 (mods, resourcepacks, shaderpacks) + const contents = { mods: [], resourcepacks: [], shaderpacks: [] }; + for (const file of indexJson.files || []) { + const path = file.path; + // 파일명에서 이름과 버전 추출 시도 (예: Create-0.5.1.jar) + const fileName = path + .split("/") + .pop() + .replace(/\.(jar|zip)$/, ""); + const match = fileName.match(/^(.+?)[-_](\d+[\d.]+\d*)$/); + const name = match ? match[1].replace(/[-_]/g, " ") : fileName; + const version = match ? match[2] : ""; + + if (path.startsWith("mods/")) { + contents.mods.push({ name, version }); + } else if (path.startsWith("resourcepacks/")) { + contents.resourcepacks.push({ name, version }); + } else if (path.startsWith("shaderpacks/")) { + contents.shaderpacks.push({ name, version }); + } + } + + // S3에 업로드 + const s3Key = `modpacks/${modpackName.replace( + /[^a-zA-Z0-9가-힣]/g, + "_" + )}/${modpackVersion}.mrpack`; + const { uploadToS3 } = await import("../lib/s3.js"); + await uploadToS3("minecraft", s3Key, fileData, "application/zip"); + + // DB에 저장 + const [result] = await pool.query( + `INSERT INTO modpacks (name, version, minecraft_version, mod_loader, changelog, file_key, file_size, contents_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + changelog = VALUES(changelog), + file_key = VALUES(file_key), + file_size = VALUES(file_size), + contents_json = VALUES(contents_json), + updated_at = CURRENT_TIMESTAMP`, + [ + modpackName, + modpackVersion, + minecraftVersion, + modLoader, + changelog, + s3Key, + fileData.length, + JSON.stringify(contents), + ] + ); + + console.log( + `[Admin] 모드팩 업로드 완료: ${modpackName} v${modpackVersion}` + ); + res.json({ + success: true, + id: result.insertId, + name: modpackName, + version: modpackVersion, + size: formatFileSize(fileData.length), + }); + } catch (error) { + console.error("[Admin] 모드팩 업로드 오류:", error); + res.status(500).json({ error: "업로드 실패: " + error.message }); + } + } +); + export default router;