feat: 아이콘/번역 삭제 시 S3 파일도 삭제, UI 용어 통일

- S3 deleteFromS3, deleteByPrefix 함수 추가
- 아이콘 삭제 시 S3 파일도 삭제
- 번역 삭제 시 아이콘(S3 파일)도 함께 삭제
- 다이얼로그 '초기화' → '삭제'로 용어 통일
- 토스트 메시지 개선
This commit is contained in:
caadiq 2025-12-26 20:14:43 +09:00
parent b511f374b5
commit 15709c4fb8
3 changed files with 96 additions and 14 deletions

View file

@ -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,
};

View file

@ -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: "삭제 실패" });
}
});

View file

@ -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()}
>
<h3 className="text-white text-lg font-medium mb-2">아이콘 초기화</h3>
<h3 className="text-white text-lg font-medium mb-2">아이콘 삭제</h3>
<p className="text-zinc-400 text-sm mb-6">
<span className="text-emerald-400 font-medium">{deleteIconDialog.modId}</span> 모드의 아이콘을 초기화하시겠습니까?
<span className="text-emerald-400 font-medium">{deleteIconDialog.modId}</span> 모드의 아이콘을 삭제하시겠습니까?
</p>
<div className="flex gap-3">
<button
@ -2788,7 +2788,7 @@ export default function Admin({ isMobile = false }) {
}}
className="flex-1 py-2.5 bg-red-600 hover:bg-red-500 text-white font-medium rounded-xl transition-colors"
>
초기화
삭제
</button>
</div>
</motion.div>