refactor(backend): 17단계 중복 코드 제거 - 멤버/앨범 조회 서비스 분리

- services/member.js 생성: getAllMembers, getMemberByName, getMemberBasicByName
- services/album.js에 getAlbumByName, getAlbumById 추가
- routes/members/index.js 서비스 호출로 변경 (약 50줄 감소)
- routes/albums/index.js 서비스 호출로 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 15:58:08 +09:00
parent 5cc258b009
commit b0ac0e51e4
5 changed files with 150 additions and 102 deletions

View file

@ -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 (인증 필요) ====================

View file

@ -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();

View file

@ -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 - 데이터베이스 연결

View file

@ -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;
}

View file

@ -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단계 | 서비스 레이어 확대 | 🔄 진행 예정 |