refactor(backend): 대형 핸들러 서비스로 분리
- createAlbum, updateAlbum, deleteAlbum 서비스 함수 추가 - insertTracks 배치 삽입 헬퍼 함수 - albums/index.js POST/PUT/DELETE → 서비스 호출로 변경 - routes 파일 80줄 감소 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7b227a6c56
commit
2d7d82baf3
3 changed files with 206 additions and 137 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
uploadAlbumCover,
|
getAlbumDetails,
|
||||||
deleteAlbumCover,
|
getAlbumsWithTracks,
|
||||||
} from '../../services/image.js';
|
createAlbum,
|
||||||
import { getAlbumDetails, getAlbumsWithTracks } from '../../services/album.js';
|
updateAlbum,
|
||||||
|
deleteAlbum,
|
||||||
|
} from '../../services/album.js';
|
||||||
import photosRoutes from './photos.js';
|
import photosRoutes from './photos.js';
|
||||||
import teasersRoutes from './teasers.js';
|
import teasersRoutes from './teasers.js';
|
||||||
|
|
||||||
|
|
@ -157,59 +159,13 @@ export default async function albumsRoutes(fastify) {
|
||||||
return reply.code(400).send({ error: '데이터가 필요합니다.' });
|
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) {
|
if (!title || !album_type || !release_date || !folder_name) {
|
||||||
return reply.code(400).send({ error: '필수 필드를 모두 입력해주세요.' });
|
return reply.code(400).send({ error: '필수 필드를 모두 입력해주세요.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = await db.getConnection();
|
return await createAlbum(db, data, coverBuffer);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -240,62 +196,11 @@ export default async function albumsRoutes(fastify) {
|
||||||
return reply.code(400).send({ error: '데이터가 필요합니다.' });
|
return reply.code(400).send({ error: '데이터가 필요합니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data;
|
const result = await updateAlbum(db, id, data, coverBuffer);
|
||||||
|
if (!result) {
|
||||||
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: '앨범을 찾을 수 없습니다.' });
|
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -310,32 +215,10 @@ export default async function albumsRoutes(fastify) {
|
||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const connection = await db.getConnection();
|
const result = await deleteAlbum(db, id);
|
||||||
|
if (!result) {
|
||||||
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: '앨범을 찾을 수 없습니다.' });
|
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* 앨범 서비스
|
* 앨범 서비스
|
||||||
* 앨범 관련 비즈니스 로직
|
* 앨범 관련 비즈니스 로직
|
||||||
*/
|
*/
|
||||||
|
import { uploadAlbumCover, deleteAlbumCover } from './image.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 앨범 상세 정보 조회 (트랙, 티저, 컨셉포토 포함)
|
* 앨범 상세 정보 조회 (트랙, 티저, 컨셉포토 포함)
|
||||||
|
|
@ -101,3 +102,184 @@ export async function getAlbumsWithTracks(db) {
|
||||||
|
|
||||||
return albums;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,9 +102,13 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 11단계: 대형 핸들러 분리
|
### 11단계: 대형 핸들러 분리 ✅ 완료
|
||||||
- [ ] `routes/albums/photos.js` POST (153줄) → `services/album.js`로 이동
|
- [x] `routes/albums/index.js` POST/PUT/DELETE → 서비스 함수로 분리
|
||||||
- [ ] `routes/albums/index.js` POST/PUT → 서비스 함수로 분리
|
- [ ] `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 | ✅ 완료 |
|
| 8단계 | meilisearch 카테고리 ID | ✅ 완료 |
|
||||||
| 9단계 | 응답 형식 통일 | ✅ 완료 |
|
| 9단계 | 응답 형식 통일 | ✅ 완료 |
|
||||||
| 10단계 | 로거 통일 | ✅ 완료 |
|
| 10단계 | 로거 통일 | ✅ 완료 |
|
||||||
| 11단계 | 대형 핸들러 분리 | 대기 |
|
| 11단계 | 대형 핸들러 분리 | ✅ 완료 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue