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,
|
PutObjectCommand,
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
HeadObjectCommand,
|
HeadObjectCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
} from "@aws-sdk/client-s3";
|
} from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
// S3 설정 (RustFS) - 환경변수에서 로드
|
// 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 - 모드 번역 삭제
|
* DELETE /api/admin/modtranslations/:modId - 모드 번역 삭제
|
||||||
|
* 번역 데이터와 함께 아이콘(S3 파일)도 삭제
|
||||||
*/
|
*/
|
||||||
router.delete("/modtranslations/:modId", requireAdmin, async (req, res) => {
|
router.delete("/modtranslations/:modId", requireAdmin, async (req, res) => {
|
||||||
const { modId } = req.params;
|
const { modId } = req.params;
|
||||||
|
|
||||||
try {
|
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();
|
await loadTranslations();
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
deleted: result.affectedRows,
|
||||||
|
s3Deleted: s3DeletedCount,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Admin] 모드 번역 삭제 오류:", error);
|
console.error("[Admin] 모드 삭제 오류:", error);
|
||||||
res.status(500).json({ error: "삭제 실패" });
|
res.status(500).json({ error: "삭제 실패" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1046,24 +1065,42 @@ router.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/admin/icons/:modId - 특정 모드 아이콘 초기화
|
* DELETE /api/admin/icons/:modId - 특정 모드 아이콘 삭제
|
||||||
* blocks/items 테이블의 icon 컬럼을 NULL로 설정
|
* items 테이블의 icon 컬럼을 NULL로 설정하고 S3에서 파일 삭제
|
||||||
*/
|
*/
|
||||||
router.delete("/icons/:modId", requireAdmin, async (req, res) => {
|
router.delete("/icons/:modId", requireAdmin, async (req, res) => {
|
||||||
const { modId } = req.params;
|
const { modId } = req.params;
|
||||||
|
|
||||||
try {
|
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로 업데이트
|
// items 테이블의 icon 컬럼 NULL로 업데이트
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`UPDATE items SET icon = NULL WHERE mod_id = ? AND icon IS NOT NULL`,
|
`UPDATE items SET icon = NULL WHERE mod_id = ? AND icon IS NOT NULL`,
|
||||||
[modId]
|
[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) {
|
} catch (error) {
|
||||||
console.error("[Admin] 아이콘 초기화 오류:", error);
|
console.error("[Admin] 아이콘 삭제 오류:", error);
|
||||||
res.status(500).json({ error: "삭제 실패" });
|
res.status(500).json({ error: "삭제 실패" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ export default function Admin({ isMobile = false }) {
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setToast(`${modId} 모드 아이콘 ${data.deleted}개 초기화`);
|
setToast(`${modId} 모드 아이콘 삭제 완료 (DB ${data.deleted}개, S3 ${data.s3Deleted}개)`);
|
||||||
fetchIconMods();
|
fetchIconMods();
|
||||||
} else {
|
} else {
|
||||||
setToast(data.error || '삭제 실패', true);
|
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"
|
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 w-full max-w-sm mx-4"
|
||||||
onClick={e => e.stopPropagation()}
|
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">
|
<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>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<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"
|
className="flex-1 py-2.5 bg-red-600 hover:bg-red-500 text-white font-medium rounded-xl transition-colors"
|
||||||
>
|
>
|
||||||
초기화
|
삭제
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue