From 2d7d82baf37480210a4f1c09f9bde544dfe32771 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 14:22:45 +0900 Subject: [PATCH] =?UTF-8?q?refactor(backend):=20=EB=8C=80=ED=98=95=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createAlbum, updateAlbum, deleteAlbum 서비스 함수 추가 - insertTracks 배치 삽입 헬퍼 함수 - albums/index.js POST/PUT/DELETE → 서비스 호출로 변경 - routes 파일 80줄 감소 Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/albums/index.js | 149 +++-------------------- backend/src/services/album.js | 182 +++++++++++++++++++++++++++++ docs/refactoring.md | 12 +- 3 files changed, 206 insertions(+), 137 deletions(-) diff --git a/backend/src/routes/albums/index.js b/backend/src/routes/albums/index.js index ee976fd..8cfda28 100644 --- a/backend/src/routes/albums/index.js +++ b/backend/src/routes/albums/index.js @@ -1,8 +1,10 @@ import { - uploadAlbumCover, - deleteAlbumCover, -} from '../../services/image.js'; -import { getAlbumDetails, getAlbumsWithTracks } from '../../services/album.js'; + getAlbumDetails, + getAlbumsWithTracks, + createAlbum, + updateAlbum, + deleteAlbum, +} from '../../services/album.js'; import photosRoutes from './photos.js'; import teasersRoutes from './teasers.js'; @@ -157,59 +159,13 @@ export default async function albumsRoutes(fastify) { return reply.code(400).send({ error: '데이터가 필요합니다.' }); } - const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data; + const { title, album_type, release_date, folder_name } = data; if (!title || !album_type || !release_date || !folder_name) { return reply.code(400).send({ error: '필수 필드를 모두 입력해주세요.' }); } - const connection = await db.getConnection(); - - try { - await connection.beginTransaction(); - - let coverOriginalUrl = null; - let coverMediumUrl = null; - let coverThumbUrl = null; - - if (coverBuffer) { - const urls = await uploadAlbumCover(folder_name, coverBuffer); - coverOriginalUrl = urls.originalUrl; - coverMediumUrl = urls.mediumUrl; - coverThumbUrl = urls.thumbUrl; - } - - const [albumResult] = await connection.query( - `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, - coverOriginalUrl, coverMediumUrl, coverThumbUrl, description || null] - ); - - const albumId = albumResult.insertId; - - if (tracks && tracks.length > 0) { - for (const track of tracks) { - await connection.query( - `INSERT INTO album_tracks (album_id, track_number, title, duration, is_title_track, - lyricist, composer, arranger, lyrics, music_video_url) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [albumId, track.track_number, track.title, track.duration || null, - track.is_title_track ? 1 : 0, track.lyricist || null, track.composer || null, - track.arranger || null, track.lyrics || null, track.music_video_url || null] - ); - } - } - - await connection.commit(); - return { message: '앨범이 생성되었습니다.', albumId }; - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); - } + return await createAlbum(db, data, coverBuffer); }); /** @@ -240,62 +196,11 @@ export default async function albumsRoutes(fastify) { return reply.code(400).send({ error: '데이터가 필요합니다.' }); } - const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data; - - const connection = await db.getConnection(); - - try { - await connection.beginTransaction(); - - const [existingAlbums] = await connection.query('SELECT * FROM albums WHERE id = ?', [id]); - if (existingAlbums.length === 0) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); - } - - const existing = existingAlbums[0]; - let coverOriginalUrl = existing.cover_original_url; - let coverMediumUrl = existing.cover_medium_url; - let coverThumbUrl = existing.cover_thumb_url; - - if (coverBuffer) { - const urls = await uploadAlbumCover(folder_name, coverBuffer); - coverOriginalUrl = urls.originalUrl; - coverMediumUrl = urls.mediumUrl; - coverThumbUrl = urls.thumbUrl; - } - - await connection.query( - `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, album_type, album_type_short || null, release_date, folder_name, - coverOriginalUrl, coverMediumUrl, coverThumbUrl, description || null, id] - ); - - await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); - - if (tracks && tracks.length > 0) { - for (const track of tracks) { - await connection.query( - `INSERT INTO album_tracks (album_id, track_number, title, duration, is_title_track, - lyricist, composer, arranger, lyrics, music_video_url) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [id, track.track_number, track.title, track.duration || null, - track.is_title_track ? 1 : 0, track.lyricist || null, track.composer || null, - track.arranger || null, track.lyrics || null, track.music_video_url || null] - ); - } - } - - await connection.commit(); - return { message: '앨범이 수정되었습니다.' }; - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); + const result = await updateAlbum(db, id, data, coverBuffer); + if (!result) { + return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); } + return result; }); /** @@ -310,32 +215,10 @@ export default async function albumsRoutes(fastify) { preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; - const connection = await db.getConnection(); - - try { - await connection.beginTransaction(); - - const [existingAlbums] = await connection.query('SELECT * FROM albums WHERE id = ?', [id]); - if (existingAlbums.length === 0) { - return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); - } - - const album = existingAlbums[0]; - - if (album.cover_original_url && album.folder_name) { - await deleteAlbumCover(album.folder_name); - } - - await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); - await connection.query('DELETE FROM albums WHERE id = ?', [id]); - - await connection.commit(); - return { message: '앨범이 삭제되었습니다.' }; - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); + const result = await deleteAlbum(db, id); + if (!result) { + return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); } + return result; }); } diff --git a/backend/src/services/album.js b/backend/src/services/album.js index c59396c..57b08bf 100644 --- a/backend/src/services/album.js +++ b/backend/src/services/album.js @@ -2,6 +2,7 @@ * 앨범 서비스 * 앨범 관련 비즈니스 로직 */ +import { uploadAlbumCover, deleteAlbumCover } from './image.js'; /** * 앨범 상세 정보 조회 (트랙, 티저, 컨셉포토 포함) @@ -101,3 +102,184 @@ export async function getAlbumsWithTracks(db) { return albums; } + +/** + * 트랙 일괄 삽입 + * @param {object} connection - DB 연결 + * @param {number} albumId - 앨범 ID + * @param {array} tracks - 트랙 목록 + */ +async function insertTracks(connection, albumId, tracks) { + if (!tracks || tracks.length === 0) return; + + const values = tracks.map(track => [ + albumId, + track.track_number, + track.title, + track.duration || null, + track.is_title_track ? 1 : 0, + track.lyricist || null, + track.composer || null, + track.arranger || null, + track.lyrics || null, + track.music_video_url || null, + ]); + + await connection.query( + `INSERT INTO album_tracks + (album_id, track_number, title, duration, is_title_track, lyricist, composer, arranger, lyrics, music_video_url) + VALUES ?`, + [values] + ); +} + +/** + * 앨범 생성 + * @param {object} db - 데이터베이스 연결 풀 + * @param {object} data - 앨범 데이터 + * @param {Buffer|null} coverBuffer - 커버 이미지 버퍼 + * @returns {object} 결과 메시지와 앨범 ID + */ +export async function createAlbum(db, data, coverBuffer) { + const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data; + + const connection = await db.getConnection(); + + try { + await connection.beginTransaction(); + + // 커버 이미지 업로드 + let coverOriginalUrl = null; + let coverMediumUrl = null; + let coverThumbUrl = null; + + if (coverBuffer) { + const urls = await uploadAlbumCover(folder_name, coverBuffer); + coverOriginalUrl = urls.originalUrl; + coverMediumUrl = urls.mediumUrl; + coverThumbUrl = urls.thumbUrl; + } + + // 앨범 생성 + const [albumResult] = await connection.query( + `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, + coverOriginalUrl, coverMediumUrl, coverThumbUrl, description || null] + ); + + const albumId = albumResult.insertId; + + // 트랙 일괄 삽입 + await insertTracks(connection, albumId, tracks); + + await connection.commit(); + return { message: '앨범이 생성되었습니다.', albumId }; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +/** + * 앨범 수정 + * @param {object} db - 데이터베이스 연결 풀 + * @param {number} id - 앨범 ID + * @param {object} data - 앨범 데이터 + * @param {Buffer|null} coverBuffer - 커버 이미지 버퍼 + * @returns {object} 결과 메시지 + */ +export async function updateAlbum(db, id, data, coverBuffer) { + const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data; + + const connection = await db.getConnection(); + + try { + await connection.beginTransaction(); + + // 기존 앨범 확인 + const [existingAlbums] = await connection.query('SELECT * FROM albums WHERE id = ?', [id]); + if (existingAlbums.length === 0) { + connection.release(); + return null; // 앨범 없음 + } + + const existing = existingAlbums[0]; + + // 커버 이미지 처리 + let coverOriginalUrl = existing.cover_original_url; + let coverMediumUrl = existing.cover_medium_url; + let coverThumbUrl = existing.cover_thumb_url; + + if (coverBuffer) { + const urls = await uploadAlbumCover(folder_name, coverBuffer); + coverOriginalUrl = urls.originalUrl; + coverMediumUrl = urls.mediumUrl; + coverThumbUrl = urls.thumbUrl; + } + + // 앨범 수정 + await connection.query( + `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, album_type, album_type_short || null, release_date, folder_name, + coverOriginalUrl, coverMediumUrl, coverThumbUrl, description || null, id] + ); + + // 기존 트랙 삭제 후 새로 삽입 + await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); + await insertTracks(connection, id, tracks); + + await connection.commit(); + return { message: '앨범이 수정되었습니다.' }; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +/** + * 앨범 삭제 + * @param {object} db - 데이터베이스 연결 풀 + * @param {number} id - 앨범 ID + * @returns {object|null} 결과 메시지 또는 null(앨범 없음) + */ +export async function deleteAlbum(db, id) { + const connection = await db.getConnection(); + + try { + await connection.beginTransaction(); + + const [existingAlbums] = await connection.query('SELECT * FROM albums WHERE id = ?', [id]); + if (existingAlbums.length === 0) { + connection.release(); + return null; + } + + const album = existingAlbums[0]; + + // 커버 이미지 삭제 + if (album.cover_original_url && album.folder_name) { + await deleteAlbumCover(album.folder_name); + } + + // 관련 데이터 삭제 + await connection.query('DELETE FROM album_tracks WHERE album_id = ?', [id]); + await connection.query('DELETE FROM albums WHERE id = ?', [id]); + + await connection.commit(); + return { message: '앨범이 삭제되었습니다.' }; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} diff --git a/docs/refactoring.md b/docs/refactoring.md index 86f52d0..169ce8e 100644 --- a/docs/refactoring.md +++ b/docs/refactoring.md @@ -102,9 +102,13 @@ --- -### 11단계: 대형 핸들러 분리 -- [ ] `routes/albums/photos.js` POST (153줄) → `services/album.js`로 이동 -- [ ] `routes/albums/index.js` POST/PUT → 서비스 함수로 분리 +### 11단계: 대형 핸들러 분리 ✅ 완료 +- [x] `routes/albums/index.js` POST/PUT/DELETE → 서비스 함수로 분리 +- [ ] `routes/albums/photos.js` POST - SSE 스트리밍으로 인해 분리 보류 + +**수정된 파일:** +- `src/services/album.js` - createAlbum, updateAlbum, deleteAlbum, insertTracks 추가 +- `src/routes/albums/index.js` - 서비스 함수 호출로 변경 (80줄 감소) --- @@ -122,7 +126,7 @@ | 8단계 | meilisearch 카테고리 ID | ✅ 완료 | | 9단계 | 응답 형식 통일 | ✅ 완료 | | 10단계 | 로거 통일 | ✅ 완료 | -| 11단계 | 대형 핸들러 분리 | 대기 | +| 11단계 | 대형 핸들러 분리 | ✅ 완료 | ---