fromis_9/backend/src/routes/albums/index.js

284 lines
7.7 KiB
JavaScript
Raw Normal View History

import {
getAlbumDetails,
getAlbumsWithTracks,
createAlbum,
updateAlbum,
deleteAlbum,
} from '../../services/album.js';
import photosRoutes from './photos.js';
import teasersRoutes from './teasers.js';
import { errorResponse, successResponse, idParam } from '../../schemas/index.js';
/**
* 앨범 라우트
* GET: 공개, POST/PUT/DELETE: 인증 필요
*/
export default async function albumsRoutes(fastify) {
const { db } = fastify;
// 하위 라우트 등록
fastify.register(photosRoutes);
fastify.register(teasersRoutes);
// ==================== GET (공개) ====================
/**
* GET /api/albums
*/
fastify.get('/', {
schema: {
tags: ['albums'],
summary: '전체 앨범 목록 조회',
description: '모든 앨범과 트랙 목록을 조회합니다.',
response: {
200: { type: 'array', items: { type: 'object', additionalProperties: true } },
},
},
}, async () => {
return await getAlbumsWithTracks(db);
});
/**
* GET /api/albums/by-name/:albumName/track/:trackTitle
*/
fastify.get('/by-name/:albumName/track/:trackTitle', {
schema: {
tags: ['albums'],
summary: '앨범명과 트랙명으로 트랙 조회',
description: '앨범명(또는 폴더명)과 트랙명으로 트랙 상세 정보를 조회합니다.',
params: {
type: 'object',
properties: {
albumName: { type: 'string', description: '앨범명 또는 폴더명' },
trackTitle: { type: 'string', description: '트랙 제목' },
},
required: ['albumName', 'trackTitle'],
},
response: {
404: errorResponse,
},
},
}, async (request, reply) => {
const albumName = decodeURIComponent(request.params.albumName);
const trackTitle = decodeURIComponent(request.params.trackTitle);
const [albums] = await db.query(
'SELECT * FROM albums WHERE folder_name = ? OR title = ?',
[albumName, albumName]
);
if (albums.length === 0) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
}
const album = albums[0];
const [tracks] = await db.query(
'SELECT * FROM album_tracks WHERE album_id = ? AND title = ?',
[album.id, trackTitle]
);
if (tracks.length === 0) {
return reply.code(404).send({ error: '트랙을 찾을 수 없습니다.' });
}
const track = tracks[0];
const [otherTracks] = await db.query(
'SELECT id, track_number, title, is_title_track, duration FROM album_tracks WHERE album_id = ? ORDER BY track_number',
[album.id]
);
return {
...track,
album: {
id: album.id,
title: album.title,
folder_name: album.folder_name,
cover_thumb_url: album.cover_thumb_url,
cover_medium_url: album.cover_medium_url,
release_date: album.release_date,
album_type: album.album_type,
},
otherTracks,
};
});
/**
* GET /api/albums/by-name/:name
*/
fastify.get('/by-name/:name', {
schema: {
tags: ['albums'],
summary: '앨범명으로 앨범 조회',
description: '앨범명(또는 폴더명)으로 앨범 상세 정보를 조회합니다.',
params: {
type: 'object',
properties: {
name: { type: 'string', description: '앨범명 또는 폴더명' },
},
required: ['name'],
},
response: {
200: { type: 'object', additionalProperties: true },
},
},
}, async (request, reply) => {
const name = decodeURIComponent(request.params.name);
const [albums] = await db.query(
'SELECT * FROM albums WHERE folder_name = ? OR title = ?',
[name, name]
);
if (albums.length === 0) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
}
return getAlbumDetails(db, albums[0]);
});
/**
* GET /api/albums/:id
*/
fastify.get('/:id', {
schema: {
tags: ['albums'],
summary: 'ID로 앨범 조회',
description: '앨범 ID로 상세 정보(트랙, 티저, 컨셉포토 포함)를 조회합니다.',
params: idParam,
response: {
200: { type: 'object', additionalProperties: true },
},
},
}, async (request, reply) => {
const [albums] = await db.query('SELECT * FROM albums WHERE id = ?', [
request.params.id,
]);
if (albums.length === 0) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
}
return getAlbumDetails(db, albums[0]);
});
// ==================== POST/PUT/DELETE (인증 필요) ====================
/**
* POST /api/albums
*/
fastify.post('/', {
schema: {
tags: ['albums'],
summary: '앨범 생성',
description: 'multipart/form-data로 앨범을 생성합니다. data 필드에 JSON, cover 필드에 이미지 파일.',
security: [{ bearerAuth: [] }],
consumes: ['multipart/form-data'],
response: {
200: {
type: 'object',
properties: {
message: { type: 'string' },
albumId: { type: 'integer' },
},
},
400: errorResponse,
},
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const parts = request.parts();
let data = null;
let coverBuffer = null;
for await (const part of parts) {
if (part.type === 'file' && part.fieldname === 'cover') {
coverBuffer = await part.toBuffer();
} else if (part.fieldname === 'data') {
data = JSON.parse(part.value);
}
}
if (!data) {
return reply.code(400).send({ error: '데이터가 필요합니다.' });
}
const { title, album_type, release_date, folder_name } = data;
if (!title || !album_type || !release_date || !folder_name) {
return reply.code(400).send({ error: '필수 필드를 모두 입력해주세요.' });
}
return await createAlbum(db, data, coverBuffer);
});
/**
* PUT /api/albums/:id
*/
fastify.put('/:id', {
schema: {
tags: ['albums'],
summary: '앨범 수정',
description: 'multipart/form-data로 앨범을 수정합니다.',
security: [{ bearerAuth: [] }],
consumes: ['multipart/form-data'],
params: idParam,
response: {
200: successResponse,
400: errorResponse,
404: errorResponse,
},
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { id } = request.params;
const parts = request.parts();
let data = null;
let coverBuffer = null;
for await (const part of parts) {
if (part.type === 'file' && part.fieldname === 'cover') {
coverBuffer = await part.toBuffer();
} else if (part.fieldname === 'data') {
data = JSON.parse(part.value);
}
}
if (!data) {
return reply.code(400).send({ error: '데이터가 필요합니다.' });
}
const result = await updateAlbum(db, id, data, coverBuffer);
if (!result) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
}
return result;
});
/**
* DELETE /api/albums/:id
*/
fastify.delete('/:id', {
schema: {
tags: ['albums'],
summary: '앨범 삭제',
description: '앨범과 관련 데이터(트랙, 커버 이미지)를 삭제합니다.',
security: [{ bearerAuth: [] }],
params: idParam,
response: {
200: successResponse,
404: errorResponse,
},
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { id } = request.params;
const result = await deleteAlbum(db, id);
if (!result) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
}
return result;
});
}