diff --git a/backend/src/routes/albums/index.js b/backend/src/routes/albums/index.js index 49ed4f3..e78f08e 100644 --- a/backend/src/routes/albums/index.js +++ b/backend/src/routes/albums/index.js @@ -1,6 +1,8 @@ import { getAlbumDetails, getAlbumsWithTracks, + getAlbumByName, + getAlbumById, createAlbum, updateAlbum, deleteAlbum, @@ -62,17 +64,11 @@ export default async function albumsRoutes(fastify) { 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) { + const album = await getAlbumByName(db, albumName); + if (!album) { 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] @@ -124,18 +120,11 @@ export default async function albumsRoutes(fastify) { }, }, }, 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) { + const album = await getAlbumByName(db, decodeURIComponent(request.params.name)); + if (!album) { return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); } - - return getAlbumDetails(db, albums[0]); + return getAlbumDetails(db, album); }); /** @@ -152,15 +141,11 @@ export default async function albumsRoutes(fastify) { }, }, }, async (request, reply) => { - const [albums] = await db.query('SELECT * FROM albums WHERE id = ?', [ - request.params.id, - ]); - - if (albums.length === 0) { + const album = await getAlbumById(db, request.params.id); + if (!album) { return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); } - - return getAlbumDetails(db, albums[0]); + return getAlbumDetails(db, album); }); // ==================== POST/PUT/DELETE (인증 필요) ==================== diff --git a/backend/src/routes/members/index.js b/backend/src/routes/members/index.js index 0d76266..00650e6 100644 --- a/backend/src/routes/members/index.js +++ b/backend/src/routes/members/index.js @@ -1,4 +1,5 @@ import { uploadMemberImage } from '../../services/image.js'; +import { getAllMembers, getMemberByName, getMemberBasicByName } from '../../services/member.js'; /** * 멤버 라우트 @@ -18,39 +19,7 @@ export default async function membersRoutes(fastify, opts) { }, }, async (request, reply) => { try { - const [members] = await db.query(` - SELECT - m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, - i.original_url as image_original, - i.medium_url as image_medium, - i.thumb_url as image_thumb - FROM members m - LEFT JOIN images i ON m.image_id = i.id - ORDER BY m.is_former ASC, m.id ASC - `); - - // 별명 조회 - const [nicknames] = await db.query( - 'SELECT member_id, nickname FROM member_nicknames' - ); - - // 멤버별 별명 매핑 - const nicknameMap = {}; - for (const n of nicknames) { - if (!nicknameMap[n.member_id]) { - nicknameMap[n.member_id] = []; - } - nicknameMap[n.member_id].push(n.nickname); - } - - // 멤버 데이터에 별명 추가 - const result = members.map(m => ({ - ...m, - nicknames: nicknameMap[m.id] || [], - image_url: m.image_thumb || m.image_medium || m.image_original, - })); - - return result; + return await getAllMembers(db); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: '멤버 목록 조회 실패' }); @@ -73,37 +42,12 @@ export default async function membersRoutes(fastify, opts) { }, }, }, async (request, reply) => { - const { name } = request.params; - try { - const [members] = await db.query(` - SELECT - m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, - i.original_url as image_original, - i.medium_url as image_medium, - i.thumb_url as image_thumb - FROM members m - LEFT JOIN images i ON m.image_id = i.id - WHERE m.name = ? - `, [decodeURIComponent(name)]); - - if (members.length === 0) { + const member = await getMemberByName(db, decodeURIComponent(request.params.name)); + if (!member) { return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); } - - const member = members[0]; - - // 별명 조회 - const [nicknames] = await db.query( - 'SELECT nickname FROM member_nicknames WHERE member_id = ?', - [member.id] - ); - - return { - ...member, - nicknames: nicknames.map(n => n.nickname), - image_url: member.image_original || member.image_medium || member.image_thumb, - }; + return member; } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: '멤버 조회 실패' }); @@ -134,17 +78,13 @@ export default async function membersRoutes(fastify, opts) { try { // 기존 멤버 조회 - const [existing] = await db.query( - 'SELECT id, image_id FROM members WHERE name = ?', - [decodedName] - ); - - if (existing.length === 0) { + const existing = await getMemberBasicByName(db, decodedName); + if (!existing) { return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); } - const memberId = existing[0].id; - let imageId = existing[0].image_id; + const memberId = existing.id; + let imageId = existing.image_id; // multipart 데이터 파싱 const parts = request.parts(); diff --git a/backend/src/services/album.js b/backend/src/services/album.js index 10ca61d..ceaa89b 100644 --- a/backend/src/services/album.js +++ b/backend/src/services/album.js @@ -5,6 +5,31 @@ import { uploadAlbumCover, deleteAlbumCover } from './image.js'; import { withTransaction } from '../utils/transaction.js'; +/** + * 앨범명 또는 폴더명으로 앨범 조회 + * @param {object} db - 데이터베이스 연결 + * @param {string} name - 앨범명 또는 폴더명 + * @returns {object|null} 앨범 정보 또는 null + */ +export async function getAlbumByName(db, name) { + const [albums] = await db.query( + 'SELECT * FROM albums WHERE folder_name = ? OR title = ?', + [name, name] + ); + return albums.length > 0 ? albums[0] : null; +} + +/** + * ID로 앨범 조회 + * @param {object} db - 데이터베이스 연결 + * @param {number} id - 앨범 ID + * @returns {object|null} 앨범 정보 또는 null + */ +export async function getAlbumById(db, id) { + const [albums] = await db.query('SELECT * FROM albums WHERE id = ?', [id]); + return albums.length > 0 ? albums[0] : null; +} + /** * 앨범 상세 정보 조회 (트랙, 티저, 컨셉포토 포함) * @param {object} db - 데이터베이스 연결 diff --git a/backend/src/services/member.js b/backend/src/services/member.js new file mode 100644 index 0000000..06939c7 --- /dev/null +++ b/backend/src/services/member.js @@ -0,0 +1,94 @@ +/** + * 멤버 서비스 + * 멤버 관련 비즈니스 로직 + */ + +/** + * 전체 멤버 목록 조회 (별명 포함) + * @param {object} db - 데이터베이스 연결 + * @returns {array} 멤버 목록 + */ +export async function getAllMembers(db) { + const [members] = await db.query(` + SELECT + m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, + i.original_url as image_original, + i.medium_url as image_medium, + i.thumb_url as image_thumb + FROM members m + LEFT JOIN images i ON m.image_id = i.id + ORDER BY m.is_former ASC, m.id ASC + `); + + // 별명 조회 + const [nicknames] = await db.query( + 'SELECT member_id, nickname FROM member_nicknames' + ); + + // 멤버별 별명 매핑 + const nicknameMap = {}; + for (const n of nicknames) { + if (!nicknameMap[n.member_id]) { + nicknameMap[n.member_id] = []; + } + nicknameMap[n.member_id].push(n.nickname); + } + + // 멤버 데이터에 별명 추가 + return members.map(m => ({ + ...m, + nicknames: nicknameMap[m.id] || [], + image_url: m.image_thumb || m.image_medium || m.image_original, + })); +} + +/** + * 이름으로 멤버 조회 (별명 포함) + * @param {object} db - 데이터베이스 연결 + * @param {string} name - 멤버 이름 + * @returns {object|null} 멤버 정보 또는 null + */ +export async function getMemberByName(db, name) { + const [members] = await db.query(` + SELECT + m.id, m.name, m.name_en, m.birth_date, m.instagram, m.image_id, m.is_former, + i.original_url as image_original, + i.medium_url as image_medium, + i.thumb_url as image_thumb + FROM members m + LEFT JOIN images i ON m.image_id = i.id + WHERE m.name = ? + `, [name]); + + if (members.length === 0) { + return null; + } + + const member = members[0]; + + // 별명 조회 + const [nicknames] = await db.query( + 'SELECT nickname FROM member_nicknames WHERE member_id = ?', + [member.id] + ); + + return { + ...member, + nicknames: nicknames.map(n => n.nickname), + image_url: member.image_original || member.image_medium || member.image_thumb, + }; +} + +/** + * ID로 멤버 기본 정보 조회 (수정용) + * @param {object} db - 데이터베이스 연결 + * @param {string} name - 멤버 이름 + * @returns {object|null} 멤버 기본 정보 또는 null + */ +export async function getMemberBasicByName(db, name) { + const [members] = await db.query( + 'SELECT id, image_id FROM members WHERE name = ?', + [name] + ); + return members.length > 0 ? members[0] : null; +} diff --git a/docs/refactoring.md b/docs/refactoring.md index d17ebe0..e29d4da 100644 --- a/docs/refactoring.md +++ b/docs/refactoring.md @@ -172,13 +172,17 @@ --- -### 17단계: 중복 코드 제거 (멤버 조회) 🔄 진행 예정 -- [ ] 멤버 조회 로직을 서비스로 분리 -- [ ] 앨범 존재 확인 로직 통합 +### 17단계: 중복 코드 제거 (멤버 조회) ✅ 완료 +- [x] 멤버 조회 로직을 서비스로 분리 +- [x] 앨범 존재 확인 로직 통합 -**대상 파일:** -- `src/services/member.js` - 신규 생성 -- `src/routes/members/index.js` - 서비스 호출로 변경 +**생성된 파일:** +- `src/services/member.js` - getAllMembers, getMemberByName, getMemberBasicByName + +**수정된 파일:** +- `src/services/album.js` - getAlbumByName, getAlbumById 추가 +- `src/routes/members/index.js` - 서비스 호출로 변경 (약 50줄 감소) +- `src/routes/albums/index.js` - 서비스 호출로 변경 --- @@ -239,8 +243,8 @@ | 13단계 | Swagger/OpenAPI 문서화 | ✅ 완료 | | 14단계 | 입력 검증 강화 (JSON Schema) | ✅ 완료 | | 15단계 | 스키마 파일 분리 | ✅ 완료 | -| 16단계 | 에러 처리 일관성 | 🔄 진행 예정 | -| 17단계 | 중복 코드 제거 (멤버 조회) | 🔄 진행 예정 | +| 16단계 | 에러 처리 일관성 | ✅ 완료 | +| 17단계 | 중복 코드 제거 (멤버 조회) | ✅ 완료 | | 18단계 | 이미지 처리 최적화 | 🔄 진행 예정 | | 19단계 | Redis 캐시 확대 | 🔄 진행 예정 | | 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 |