/** * 앨범 서비스 * 앨범 관련 비즈니스 로직 */ import { uploadAlbumCover, deleteAlbumCover } from './image.js'; import { withTransaction } from '../utils/transaction.js'; /** * 앨범 상세 정보 조회 (트랙, 티저, 컨셉포토 포함) * @param {object} db - 데이터베이스 연결 * @param {object} album - 앨범 기본 정보 * @returns {object} 상세 정보가 포함된 앨범 */ export async function getAlbumDetails(db, album) { // 트랙, 티저, 포토 병렬 조회 const [[tracks], [teasers], [photos]] = await Promise.all([ db.query( 'SELECT * FROM album_tracks WHERE album_id = ? ORDER BY track_number', [album.id] ), db.query( `SELECT original_url, medium_url, thumb_url, video_url, media_type FROM album_teasers WHERE album_id = ? ORDER BY sort_order`, [album.id] ), db.query( `SELECT p.id, p.original_url, p.medium_url, p.thumb_url, p.photo_type, p.concept_name, p.sort_order, p.width, p.height, GROUP_CONCAT(m.name ORDER BY m.id SEPARATOR ', ') as members FROM album_photos p LEFT JOIN album_photo_members pm ON p.id = pm.photo_id LEFT JOIN members m ON pm.member_id = m.id WHERE p.album_id = ? GROUP BY p.id ORDER BY p.sort_order`, [album.id] ), ]); album.tracks = tracks; album.teasers = teasers; const conceptPhotos = {}; for (const photo of photos) { const concept = photo.concept_name || 'Default'; if (!conceptPhotos[concept]) { conceptPhotos[concept] = []; } conceptPhotos[concept].push({ id: photo.id, original_url: photo.original_url, medium_url: photo.medium_url, thumb_url: photo.thumb_url, width: photo.width, height: photo.height, type: photo.photo_type, members: photo.members, sortOrder: photo.sort_order, }); } album.conceptPhotos = conceptPhotos; return album; } /** * 앨범 목록과 트랙 조회 (N+1 최적화) * @param {object} db - 데이터베이스 연결 * @returns {array} 트랙 포함된 앨범 목록 */ export async function getAlbumsWithTracks(db) { const [albums] = await db.query(` SELECT id, title, folder_name, album_type, album_type_short, release_date, cover_original_url, cover_medium_url, cover_thumb_url, description FROM albums ORDER BY release_date DESC `); if (albums.length === 0) return albums; // 모든 트랙을 한 번에 조회 const albumIds = albums.map(a => a.id); const [allTracks] = await db.query( `SELECT id, album_id, track_number, title, is_title_track, duration, lyricist, composer, arranger FROM album_tracks WHERE album_id IN (?) ORDER BY album_id, track_number`, [albumIds] ); // 앨범 ID별로 트랙 그룹화 const tracksByAlbum = {}; for (const track of allTracks) { if (!tracksByAlbum[track.album_id]) { tracksByAlbum[track.album_id] = []; } tracksByAlbum[track.album_id].push(track); } // 각 앨범에 트랙 할당 for (const album of albums) { album.tracks = tracksByAlbum[album.id] || []; } 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; return withTransaction(db, async (connection) => { // 커버 이미지 업로드 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); return { message: '앨범이 생성되었습니다.', albumId }; }); } /** * 앨범 수정 * @param {object} db - 데이터베이스 연결 풀 * @param {number} id - 앨범 ID * @param {object} data - 앨범 데이터 * @param {Buffer|null} coverBuffer - 커버 이미지 버퍼 * @returns {object|null} 결과 메시지 또는 null(앨범 없음) */ export async function updateAlbum(db, id, data, coverBuffer) { const { title, album_type, album_type_short, release_date, folder_name, description, tracks } = data; // 앨범 존재 여부 먼저 확인 (트랜잭션 외부) const [existingAlbums] = await db.query('SELECT * FROM albums WHERE id = ?', [id]); if (existingAlbums.length === 0) { return null; } const existing = existingAlbums[0]; return withTransaction(db, async (connection) => { // 커버 이미지 처리 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); return { message: '앨범이 수정되었습니다.' }; }); } /** * 앨범 삭제 * @param {object} db - 데이터베이스 연결 풀 * @param {number} id - 앨범 ID * @returns {object|null} 결과 메시지 또는 null(앨범 없음) */ export async function deleteAlbum(db, id) { // 앨범 존재 여부 먼저 확인 (트랜잭션 외부) const [existingAlbums] = await db.query('SELECT * FROM albums WHERE id = ?', [id]); if (existingAlbums.length === 0) { return null; } const album = existingAlbums[0]; return withTransaction(db, async (connection) => { // 커버 이미지 삭제 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]); return { message: '앨범이 삭제되었습니다.' }; }); }