feat: 아이콘/번역 삭제 시 S3 파일도 삭제, UI 용어 통일
- S3 deleteFromS3, deleteByPrefix 함수 추가 - 아이콘 삭제 시 S3 파일도 삭제 - 번역 삭제 시 아이콘(S3 파일)도 함께 삭제 - 다이얼로그 '초기화' → '삭제'로 용어 통일 - 토스트 메시지 개선
This commit is contained in:
parent
b511f374b5
commit
15709c4fb8
3 changed files with 96 additions and 14 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: "삭제 실패" });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue