diff --git a/backend/lib/s3.js b/backend/lib/s3.js index 08e1ba9..8d7ddbf 100644 --- a/backend/lib/s3.js +++ b/backend/lib/s3.js @@ -3,6 +3,8 @@ import { PutObjectCommand, GetObjectCommand, HeadObjectCommand, + DeleteObjectCommand, + ListObjectsV2Command, } from "@aws-sdk/client-s3"; // S3 설정 (RustFS) - 환경변수에서 로드 @@ -87,4 +89,47 @@ async function checkS3Exists(bucket, key) { } } -export { s3Config, uploadToS3, downloadFromS3, checkS3Exists }; +/** + * S3(RustFS)에서 파일 삭제 + */ +async function deleteFromS3(bucket, key) { + const command = new DeleteObjectCommand({ + Bucket: bucket, + Key: key, + }); + await s3Client.send(command); +} + +/** + * S3(RustFS)에서 prefix로 시작하는 모든 파일 삭제 + */ +async function deleteByPrefix(bucket, prefix) { + const listCommand = new ListObjectsV2Command({ + Bucket: bucket, + Prefix: prefix, + }); + + const listResult = await s3Client.send(listCommand); + const objects = listResult.Contents || []; + + let deletedCount = 0; + for (const obj of objects) { + try { + await deleteFromS3(bucket, obj.Key); + deletedCount++; + } catch (err) { + console.error(`[S3] 파일 삭제 실패: ${obj.Key} - ${err.message}`); + } + } + + return deletedCount; +} + +export { + s3Config, + uploadToS3, + downloadFromS3, + checkS3Exists, + deleteFromS3, + deleteByPrefix, +}; diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 632e746..26bd3cf 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -833,21 +833,40 @@ router.post("/modtranslations", requireAdmin, async (req, res) => { /** * DELETE /api/admin/modtranslations/:modId - 모드 번역 삭제 + * 번역 데이터와 함께 아이콘(S3 파일)도 삭제 */ router.delete("/modtranslations/:modId", requireAdmin, async (req, res) => { const { modId } = req.params; try { - await pool.query(`DELETE FROM items WHERE mod_id = ?`, [modId]); + const { deleteByPrefix } = await import("../lib/s3.js"); - console.log(`[Admin] 모드 번역 삭제: ${modId}`); + // S3에서 해당 모드의 아이콘 파일 삭제 + const s3DeletedCount = await deleteByPrefix( + "minecraft", + `icons/items/${modId}_` + ); + console.log(`[Admin] S3 아이콘 파일 삭제: ${modId} (${s3DeletedCount}개)`); + + // items 테이블에서 해당 모드 삭제 + const [result] = await pool.query(`DELETE FROM items WHERE mod_id = ?`, [ + modId, + ]); + + console.log( + `[Admin] 모드 삭제: ${modId} (DB ${result.affectedRows}개, S3 ${s3DeletedCount}개)` + ); // 번역 캐시 새로고침 await loadTranslations(); - res.json({ success: true }); + res.json({ + success: true, + deleted: result.affectedRows, + s3Deleted: s3DeletedCount, + }); } catch (error) { - console.error("[Admin] 모드 번역 삭제 오류:", error); + console.error("[Admin] 모드 삭제 오류:", error); res.status(500).json({ error: "삭제 실패" }); } }); @@ -1046,24 +1065,42 @@ router.post( ); /** - * DELETE /api/admin/icons/:modId - 특정 모드 아이콘 초기화 - * blocks/items 테이블의 icon 컬럼을 NULL로 설정 + * DELETE /api/admin/icons/:modId - 특정 모드 아이콘 삭제 + * items 테이블의 icon 컬럼을 NULL로 설정하고 S3에서 파일 삭제 */ router.delete("/icons/:modId", requireAdmin, async (req, res) => { const { modId } = req.params; try { + const { deleteByPrefix } = await import("../lib/s3.js"); + + // S3에서 해당 모드의 아이콘 파일 삭제 + const s3DeletedCount = await deleteByPrefix( + "minecraft", + `icons/items/${modId}_` + ); + console.log(`[Admin] S3 아이콘 파일 삭제: ${modId} (${s3DeletedCount}개)`); + // items 테이블의 icon 컬럼 NULL로 업데이트 const [result] = await pool.query( `UPDATE items SET icon = NULL WHERE mod_id = ? AND icon IS NOT NULL`, [modId] ); - console.log(`[Admin] 아이콘 초기화: ${modId} (${result.affectedRows}개)`); + // 번역 캐시 새로고침 + await loadTranslations(); - res.json({ success: true, deleted: result.affectedRows }); + console.log( + `[Admin] 아이콘 삭제: ${modId} (DB ${result.affectedRows}개, S3 ${s3DeletedCount}개)` + ); + + res.json({ + success: true, + deleted: result.affectedRows, + s3Deleted: s3DeletedCount, + }); } catch (error) { - console.error("[Admin] 아이콘 초기화 오류:", error); + console.error("[Admin] 아이콘 삭제 오류:", error); res.status(500).json({ error: "삭제 실패" }); } }); diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 0ee1669..ee1956f 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -422,7 +422,7 @@ export default function Admin({ isMobile = false }) { const data = await res.json(); if (data.success) { - setToast(`${modId} 모드 아이콘 ${data.deleted}개 초기화`); + setToast(`${modId} 모드 아이콘 삭제 완료 (DB ${data.deleted}개, S3 ${data.s3Deleted}개)`); fetchIconMods(); } else { setToast(data.error || '삭제 실패', true); @@ -2770,9 +2770,9 @@ export default function Admin({ isMobile = false }) { className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 w-full max-w-sm mx-4" onClick={e => e.stopPropagation()} > -

아이콘 초기화

+

아이콘 삭제

- {deleteIconDialog.modId} 모드의 아이콘을 초기화하시겠습니까? + {deleteIconDialog.modId} 모드의 아이콘을 삭제하시겠습니까?