이미지 서비스 통합: boss-crystal/image.js 제거 + safeDelete 헬퍼 추가
- services/boss-crystal/image.js의 uploadBossImage/deleteBossImage는 services/image.js의 convertAndUploadTo와 safeDelete로 대체 가능해서 제거 - services/image.js에 safeDelete(path) 헬퍼 추가 (삭제 실패해도 흐름을 끊지 않고 warn 로그). 기존에 try/catch 인라인으로 흩어져있던 세 곳 통일 - routes/admin/boss-crystal.js에 BOSS_IMAGE_PREFIX 상수 인라인 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b3907ec48f
commit
c072cccf44
5 changed files with 26 additions and 46 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { Image, Menu } from '../models/index.js';
|
||||
import { convertAndUpload, deleteFromS3 } from '../services/image.js';
|
||||
import { convertAndUpload, safeDelete } from '../services/image.js';
|
||||
import { getPublicUrl } from '../lib/s3.js';
|
||||
import { sequelize } from '../lib/db.js';
|
||||
import bossCrystalRouter from './admin/boss-crystal.js';
|
||||
|
|
@ -165,13 +165,7 @@ router.post('/images/delete', async (req, res) => {
|
|||
try {
|
||||
const images = await Image.findAll({ where: { id: ids } });
|
||||
|
||||
await Promise.all(
|
||||
images.map((img) =>
|
||||
deleteFromS3(img.path).catch((err) =>
|
||||
console.warn(`S3 삭제 실패 (${img.path}):`, err.message)
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(images.map((img) => safeDelete(img.path)));
|
||||
await Image.destroy({ where: { id: ids } });
|
||||
|
||||
res.json({ success: true, deleted: images.length });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { BossCrystalBoss, BossCrystalBossDifficulty } from '../../models/index.js';
|
||||
import { uploadBossImage, deleteBossImage } from '../../services/boss-crystal/image.js';
|
||||
import { convertAndUploadTo, safeDelete } from '../../services/image.js';
|
||||
|
||||
const BOSS_IMAGE_PREFIX = 'crystal/boss';
|
||||
const bossImagePath = (name) => `${BOSS_IMAGE_PREFIX}/${name}.webp`;
|
||||
import { getPublicUrl } from '../../lib/s3.js';
|
||||
import { sequelize } from '../../lib/db.js';
|
||||
import { UPLOAD_FILE_SIZE_LIMIT, PARTY_SIZE, DIFFICULTIES } from '../../constants.js';
|
||||
|
|
@ -101,7 +104,7 @@ router.post('/bosses', upload.single('image'), async (req, res) => {
|
|||
if (existing) return res.status(400).json({ error: '같은 이름의 보스가 이미 존재합니다' });
|
||||
|
||||
// 이미지 업로드
|
||||
const imagePath = await uploadBossImage(req.file.buffer, trimmedName);
|
||||
const { path: imagePath } = await convertAndUploadTo(req.file.buffer, bossImagePath(trimmedName));
|
||||
|
||||
// 마지막 정렬 순서
|
||||
const max = await BossCrystalBoss.max('sort_order') || 0;
|
||||
|
|
@ -155,15 +158,14 @@ router.patch('/bosses/:id', upload.single('image'), async (req, res) => {
|
|||
// 새 이미지 업로드 또는 이름 변경 시 이미지 재업로드
|
||||
if (req.file) {
|
||||
const oldPath = boss.image_path;
|
||||
newImagePath = await uploadBossImage(req.file.buffer, newName);
|
||||
const uploaded = await convertAndUploadTo(req.file.buffer, bossImagePath(newName));
|
||||
newImagePath = uploaded.path;
|
||||
if (oldPath && oldPath !== newImagePath) {
|
||||
await deleteBossImage(oldPath);
|
||||
await safeDelete(oldPath);
|
||||
}
|
||||
} else if (newName !== boss.name && boss.image_path) {
|
||||
// 이름만 변경 - 기존 이미지를 새 경로로 복사하는 대신 이름 기반 경로 업데이트
|
||||
// 간단하게 처리: 기존 키를 새 키로 교체할 수 없으니, 이미지가 없으면 그대로, 있으면 path만 갱신
|
||||
newImagePath = `crystal/boss/${newName}.webp`;
|
||||
// 실제로 이름이 바뀌면 이미지를 다시 올려달라고 하는 게 안전. 현재는 path만 업데이트하고 추후 다음 업로드 시 새 경로로 저장됨
|
||||
// 이름 변경 시 path만 갱신 (실제 파일은 다음 이미지 업로드 때 새 경로로 저장됨)
|
||||
newImagePath = bossImagePath(newName);
|
||||
}
|
||||
|
||||
await sequelize.transaction(async (tx) => {
|
||||
|
|
@ -196,7 +198,7 @@ router.delete('/bosses/:id', async (req, res) => {
|
|||
if (!boss) return res.status(404).json({ error: '보스를 찾을 수 없습니다' });
|
||||
|
||||
if (boss.image_path) {
|
||||
await deleteBossImage(boss.image_path);
|
||||
await safeDelete(boss.image_path);
|
||||
}
|
||||
await boss.destroy();
|
||||
res.json({ success: true });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { Symbol, SymbolLevel } from '../../models/index.js';
|
||||
import { convertAndUploadTo, deleteFromS3 } from '../../services/image.js';
|
||||
import { convertAndUploadTo, safeDelete } from '../../services/image.js';
|
||||
import { getPublicUrl } from '../../lib/s3.js';
|
||||
import { sequelize } from '../../lib/db.js';
|
||||
import { UPLOAD_FILE_SIZE_LIMIT, SYMBOL_MASTER_LEVEL } from '../../constants.js';
|
||||
|
|
@ -167,7 +167,7 @@ router.patch('/symbols/:id', upload.single('image'), async (req, res) => {
|
|||
imageKey = imagePath(basic.type, basic.region);
|
||||
await convertAndUploadTo(req.file.buffer, imageKey);
|
||||
if (row.image && row.image !== imageKey) {
|
||||
try { await deleteFromS3(row.image); } catch { /* ignore */ }
|
||||
await safeDelete(row.image);
|
||||
}
|
||||
} else if (basic.type !== row.type || basic.region !== row.region) {
|
||||
// 이름/종류 변경 시 새 경로로 rename 대체 불가 → 기존 키 유지
|
||||
|
|
@ -211,7 +211,7 @@ router.delete('/symbols/:id', async (req, res) => {
|
|||
if (!row) return res.status(404).json({ error: '심볼을 찾을 수 없습니다' });
|
||||
const key = row.image;
|
||||
await row.destroy();
|
||||
if (key) { try { await deleteFromS3(key); } catch { /* ignore */ } }
|
||||
if (key) await safeDelete(key);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('심볼 삭제 오류:', err.message);
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import sharp from 'sharp';
|
||||
import { uploadObject, deleteObject } from '../../lib/s3.js';
|
||||
|
||||
const BOSS_IMAGE_PREFIX = 'crystal/boss';
|
||||
|
||||
/**
|
||||
* 보스 이미지를 webp로 변환하고 RustFS의 crystal/boss/{name}.webp에 업로드
|
||||
* @param {Buffer} buffer
|
||||
* @param {string} bossName
|
||||
* @returns {Promise<string>} S3 키 (예: crystal/boss/검은마법사.webp)
|
||||
*/
|
||||
export async function uploadBossImage(buffer, bossName) {
|
||||
const webp = await sharp(buffer).webp({ quality: 90 }).toBuffer();
|
||||
const path = `${BOSS_IMAGE_PREFIX}/${bossName}.webp`;
|
||||
await uploadObject(path, webp, 'image/webp');
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function deleteBossImage(path) {
|
||||
if (!path) return;
|
||||
try {
|
||||
await deleteObject(path);
|
||||
} catch (err) {
|
||||
console.warn(`보스 이미지 삭제 실패 (${path}):`, err.message);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,16 @@ export async function deleteFromS3(path) {
|
|||
await deleteObject(path);
|
||||
}
|
||||
|
||||
// 삭제 실패해도 흐름을 끊지 않는 버전 (이전 이미지 정리 등에 사용)
|
||||
export async function safeDelete(path) {
|
||||
if (!path) return;
|
||||
try {
|
||||
await deleteObject(path);
|
||||
} catch (err) {
|
||||
console.warn(`S3 삭제 실패 (${path}):`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정한 경로로 webp 변환 후 업로드 (덮어쓰기)
|
||||
* @param {Buffer} buffer - 원본 이미지 버퍼
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue