diff --git a/backend/routes/admin.js b/backend/routes/admin.js
index 526e2a2..3159013 100644
--- a/backend/routes/admin.js
+++ b/backend/routes/admin.js
@@ -171,40 +171,75 @@ router.post(
.json({ error: "필수 필드를 모두 입력해주세요." });
}
- let coverUrl = null;
+ let coverOriginalUrl = null;
+ let coverMediumUrl = null;
+ let coverThumbUrl = null;
- // 커버 이미지 업로드
+ // 커버 이미지 업로드 (3개 해상도)
if (req.file) {
- // WebP 변환 (원본 크기, 무손실)
- const processedImage = await sharp(req.file.buffer)
- .webp({ lossless: true })
- .toBuffer();
+ // 3가지 크기로 변환 (병렬)
+ const [originalBuffer, mediumBuffer, thumbBuffer] = await Promise.all([
+ sharp(req.file.buffer).webp({ lossless: true }).toBuffer(),
+ sharp(req.file.buffer)
+ .resize(800, null, { withoutEnlargement: true })
+ .webp({ quality: 85 })
+ .toBuffer(),
+ sharp(req.file.buffer)
+ .resize(400, null, { withoutEnlargement: true })
+ .webp({ quality: 80 })
+ .toBuffer(),
+ ]);
- const coverKey = `album/${folder_name}/cover.webp`;
+ const basePath = `album/${folder_name}/cover`;
- await s3Client.send(
- new PutObjectCommand({
- Bucket: BUCKET,
- Key: coverKey,
- Body: processedImage,
- ContentType: "image/webp",
- })
- );
+ // S3 업로드 (병렬)
+ await Promise.all([
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/original/cover.webp`,
+ Body: originalBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/medium_800/cover.webp`,
+ Body: mediumBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/thumb_400/cover.webp`,
+ Body: thumbBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ ]);
- coverUrl = `${process.env.RUSTFS_ENDPOINT}/${BUCKET}/${coverKey}`;
+ const publicUrl =
+ process.env.RUSTFS_PUBLIC_URL || process.env.RUSTFS_ENDPOINT;
+ coverOriginalUrl = `${publicUrl}/${BUCKET}/${basePath}/original/cover.webp`;
+ coverMediumUrl = `${publicUrl}/${BUCKET}/${basePath}/medium_800/cover.webp`;
+ coverThumbUrl = `${publicUrl}/${BUCKET}/${basePath}/thumb_400/cover.webp`;
}
// 앨범 삽입
const [albumResult] = await connection.query(
- `INSERT INTO albums (title, album_type, album_type_short, release_date, folder_name, cover_url, description)
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ `INSERT INTO albums (title, album_type, album_type_short, release_date, folder_name, cover_original_url, cover_medium_url, cover_thumb_url, description)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
title,
album_type,
album_type_short || null,
release_date,
folder_name,
- coverUrl,
+ coverOriginalUrl,
+ coverMediumUrl,
+ coverThumbUrl,
description || null,
]
);
@@ -278,31 +313,65 @@ router.put(
return res.status(404).json({ error: "앨범을 찾을 수 없습니다." });
}
- let coverUrl = existingAlbums[0].cover_url;
+ let coverOriginalUrl = existingAlbums[0].cover_original_url;
+ let coverMediumUrl = existingAlbums[0].cover_medium_url;
+ let coverThumbUrl = existingAlbums[0].cover_thumb_url;
- // WebP 변환 (원본 크기, 무손실)
+ // 커버 이미지 업로드 (3개 해상도)
if (req.file) {
- const processedImage = await sharp(req.file.buffer)
- .webp({ lossless: true })
- .toBuffer();
+ // 3가지 크기로 변환 (병렬)
+ const [originalBuffer, mediumBuffer, thumbBuffer] = await Promise.all([
+ sharp(req.file.buffer).webp({ lossless: true }).toBuffer(),
+ sharp(req.file.buffer)
+ .resize(800, null, { withoutEnlargement: true })
+ .webp({ quality: 85 })
+ .toBuffer(),
+ sharp(req.file.buffer)
+ .resize(400, null, { withoutEnlargement: true })
+ .webp({ quality: 80 })
+ .toBuffer(),
+ ]);
- const coverKey = `album/${folder_name}/cover.webp`;
+ const basePath = `album/${folder_name}/cover`;
- await s3Client.send(
- new PutObjectCommand({
- Bucket: BUCKET,
- Key: coverKey,
- Body: processedImage,
- ContentType: "image/webp",
- })
- );
+ // S3 업로드 (병렬)
+ await Promise.all([
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/original/cover.webp`,
+ Body: originalBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/medium_800/cover.webp`,
+ Body: mediumBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ s3Client.send(
+ new PutObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/thumb_400/cover.webp`,
+ Body: thumbBuffer,
+ ContentType: "image/webp",
+ })
+ ),
+ ]);
- coverUrl = `${process.env.RUSTFS_ENDPOINT}/${BUCKET}/${coverKey}`;
+ const publicUrl =
+ process.env.RUSTFS_PUBLIC_URL || process.env.RUSTFS_ENDPOINT;
+ coverOriginalUrl = `${publicUrl}/${BUCKET}/${basePath}/original/cover.webp`;
+ coverMediumUrl = `${publicUrl}/${BUCKET}/${basePath}/medium_800/cover.webp`;
+ coverThumbUrl = `${publicUrl}/${BUCKET}/${basePath}/thumb_400/cover.webp`;
}
// 앨범 업데이트
await connection.query(
- `UPDATE albums SET title = ?, album_type = ?, album_type_short = ?, release_date = ?, folder_name = ?, cover_url = ?, description = ?
+ `UPDATE albums SET title = ?, album_type = ?, album_type_short = ?, release_date = ?, folder_name = ?, cover_original_url = ?, cover_medium_url = ?, cover_thumb_url = ?, description = ?
WHERE id = ?`,
[
title,
@@ -310,7 +379,9 @@ router.put(
album_type_short || null,
release_date,
folder_name,
- coverUrl,
+ coverOriginalUrl,
+ coverMediumUrl,
+ coverThumbUrl,
description || null,
albumId,
]
@@ -375,17 +446,21 @@ router.delete("/albums/:id", authenticateToken, async (req, res) => {
const album = existingAlbums[0];
- // RustFS에서 커버 이미지 삭제
- if (album.cover_url && album.folder_name) {
- try {
- await s3Client.send(
- new DeleteObjectCommand({
- Bucket: BUCKET,
- Key: `album/${album.folder_name}/cover.webp`,
- })
- );
- } catch (s3Error) {
- console.error("S3 삭제 오류:", s3Error);
+ // RustFS에서 커버 이미지 삭제 (3가지 크기)
+ if (album.cover_original_url && album.folder_name) {
+ const basePath = `album/${album.folder_name}/cover`;
+ const sizes = ["original", "medium_800", "thumb_400"];
+ for (const size of sizes) {
+ try {
+ await s3Client.send(
+ new DeleteObjectCommand({
+ Bucket: BUCKET,
+ Key: `${basePath}/${size}/cover.webp`,
+ })
+ );
+ } catch (s3Error) {
+ console.error(`S3 커버 삭제 오류 (${size}):`, s3Error);
+ }
}
}
diff --git a/backend/routes/albums.js b/backend/routes/albums.js
index 669de07..a58440a 100644
--- a/backend/routes/albums.js
+++ b/backend/routes/albums.js
@@ -62,7 +62,7 @@ async function getAlbumDetails(album) {
router.get("/", async (req, res) => {
try {
const [albums] = await pool.query(
- "SELECT id, title, album_type, album_type_short, release_date, cover_url FROM albums ORDER BY release_date DESC"
+ "SELECT id, title, album_type, album_type_short, release_date, cover_original_url, cover_medium_url, cover_thumb_url FROM albums ORDER BY release_date DESC"
);
// 각 앨범에 트랙 정보 추가
diff --git a/frontend/src/pages/pc/Album.jsx b/frontend/src/pages/pc/Album.jsx
index 88eb6f6..fbc367c 100644
--- a/frontend/src/pages/pc/Album.jsx
+++ b/frontend/src/pages/pc/Album.jsx
@@ -121,7 +121,7 @@ function Album() {
style={{ viewTransitionName: `album-cover-${album.id}` }}
>
diff --git a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx
index abaddcf..727b133 100644
--- a/frontend/src/pages/pc/admin/AdminAlbumForm.jsx
+++ b/frontend/src/pages/pc/admin/AdminAlbumForm.jsx
@@ -371,7 +371,9 @@ function AdminAlbumForm() {
album_type: '',
album_type_short: '',
release_date: '',
- cover_url: '',
+ cover_original_url: '',
+ cover_medium_url: '',
+ cover_thumb_url: '',
folder_name: '',
description: '',
});
@@ -399,12 +401,14 @@ function AdminAlbumForm() {
album_type: data.album_type || '',
album_type_short: data.album_type_short || '',
release_date: data.release_date ? data.release_date.split('T')[0] : '',
- cover_url: data.cover_url || '',
+ cover_original_url: data.cover_original_url || '',
+ cover_medium_url: data.cover_medium_url || '',
+ cover_thumb_url: data.cover_thumb_url || '',
folder_name: data.folder_name || '',
description: data.description || '',
});
- if (data.cover_url) {
- setCoverPreview(data.cover_url);
+ if (data.cover_medium_url || data.cover_original_url) {
+ setCoverPreview(data.cover_medium_url || data.cover_original_url);
}
setTracks(data.tracks || []);
setLoading(false);
diff --git a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx
index a6f9978..de98d7c 100644
--- a/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx
+++ b/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx
@@ -720,7 +720,7 @@ function AdminAlbumPhotos() {
>