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 { import {
getAlbumDetails, getAlbumDetails,
getAlbumsWithTracks, getAlbumsWithTracks,
getAlbumByName,
getAlbumById,
createAlbum, createAlbum,
updateAlbum, updateAlbum,
deleteAlbum, deleteAlbum,
@ -62,17 +64,11 @@ export default async function albumsRoutes(fastify) {
const albumName = decodeURIComponent(request.params.albumName); const albumName = decodeURIComponent(request.params.albumName);
const trackTitle = decodeURIComponent(request.params.trackTitle); const trackTitle = decodeURIComponent(request.params.trackTitle);
const [albums] = await db.query( const album = await getAlbumByName(db, albumName);
'SELECT * FROM albums WHERE folder_name = ? OR title = ?', if (!album) {
[albumName, albumName]
);
if (albums.length === 0) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
} }
const album = albums[0];
const [tracks] = await db.query( const [tracks] = await db.query(
'SELECT * FROM album_tracks WHERE album_id = ? AND title = ?', 'SELECT * FROM album_tracks WHERE album_id = ? AND title = ?',
[album.id, trackTitle] [album.id, trackTitle]
@ -124,18 +120,11 @@ export default async function albumsRoutes(fastify) {
}, },
}, },
}, async (request, reply) => { }, async (request, reply) => {
const name = decodeURIComponent(request.params.name); const album = await getAlbumByName(db, decodeURIComponent(request.params.name));
if (!album) {
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 reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
} }
return getAlbumDetails(db, album);
return getAlbumDetails(db, albums[0]);
}); });
/** /**
@ -152,15 +141,11 @@ export default async function albumsRoutes(fastify) {
}, },
}, },
}, async (request, reply) => { }, async (request, reply) => {
const [albums] = await db.query('SELECT * FROM albums WHERE id = ?', [ const album = await getAlbumById(db, request.params.id);
request.params.id, if (!album) {
]);
if (albums.length === 0) {
return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' }); return reply.code(404).send({ error: '앨범을 찾을 수 없습니다.' });
} }
return getAlbumDetails(db, album);
return getAlbumDetails(db, albums[0]);
}); });
// ==================== POST/PUT/DELETE (인증 필요) ==================== // ==================== POST/PUT/DELETE (인증 필요) ====================

View file

@ -1,4 +1,5 @@
import { uploadMemberImage } from '../../services/image.js'; 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) => { }, async (request, reply) => {
try { try {
const [members] = await db.query(` return await getAllMembers(db);
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;
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.code(500).send({ error: '멤버 목록 조회 실패' }); return reply.code(500).send({ error: '멤버 목록 조회 실패' });
@ -73,37 +42,12 @@ export default async function membersRoutes(fastify, opts) {
}, },
}, },
}, async (request, reply) => { }, async (request, reply) => {
const { name } = request.params;
try { try {
const [members] = await db.query(` const member = await getMemberByName(db, decodeURIComponent(request.params.name));
SELECT if (!member) {
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) {
return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' });
} }
return member;
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,
};
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.code(500).send({ error: '멤버 조회 실패' }); return reply.code(500).send({ error: '멤버 조회 실패' });
@ -134,17 +78,13 @@ export default async function membersRoutes(fastify, opts) {
try { try {
// 기존 멤버 조회 // 기존 멤버 조회
const [existing] = await db.query( const existing = await getMemberBasicByName(db, decodedName);
'SELECT id, image_id FROM members WHERE name = ?', if (!existing) {
[decodedName]
);
if (existing.length === 0) {
return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' }); return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' });
} }
const memberId = existing[0].id; const memberId = existing.id;
let imageId = existing[0].image_id; let imageId = existing.image_id;
// multipart 데이터 파싱 // multipart 데이터 파싱
const parts = request.parts(); const parts = request.parts();

View file

@ -5,6 +5,31 @@
import { uploadAlbumCover, deleteAlbumCover } from './image.js'; import { uploadAlbumCover, deleteAlbumCover } from './image.js';
import { withTransaction } from '../utils/transaction.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 - 데이터베이스 연결 * @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/services/member.js` - getAllMembers, getMemberByName, getMemberBasicByName
- `src/routes/members/index.js` - 서비스 호출로 변경
**수정된 파일:**
- `src/services/album.js` - getAlbumByName, getAlbumById 추가
- `src/routes/members/index.js` - 서비스 호출로 변경 (약 50줄 감소)
- `src/routes/albums/index.js` - 서비스 호출로 변경
--- ---
@ -239,8 +243,8 @@
| 13단계 | Swagger/OpenAPI 문서화 | ✅ 완료 | | 13단계 | Swagger/OpenAPI 문서화 | ✅ 완료 |
| 14단계 | 입력 검증 강화 (JSON Schema) | ✅ 완료 | | 14단계 | 입력 검증 강화 (JSON Schema) | ✅ 완료 |
| 15단계 | 스키마 파일 분리 | ✅ 완료 | | 15단계 | 스키마 파일 분리 | ✅ 완료 |
| 16단계 | 에러 처리 일관성 | 🔄 진행 예정 | | 16단계 | 에러 처리 일관성 | ✅ 완료 |
| 17단계 | 중복 코드 제거 (멤버 조회) | 🔄 진행 예정 | | 17단계 | 중복 코드 제거 (멤버 조회) | ✅ 완료 |
| 18단계 | 이미지 처리 최적화 | 🔄 진행 예정 | | 18단계 | 이미지 처리 최적화 | 🔄 진행 예정 |
| 19단계 | Redis 캐시 확대 | 🔄 진행 예정 | | 19단계 | Redis 캐시 확대 | 🔄 진행 예정 |
| 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 | | 20단계 | 서비스 레이어 확대 | 🔄 진행 예정 |