refactor(backend): 에러 처리 통일

- src/utils/error.js 생성: 에러 응답 유틸리티 함수들
- reply.status() → reply.code() 통일 (auth, members, stats)
- Fastify 표준 에러 응답 패턴 적용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-21 13:43:26 +09:00
parent 430bf38c91
commit 2f30c67b93
5 changed files with 72 additions and 17 deletions

View file

@ -42,7 +42,7 @@ export default async function authRoutes(fastify, opts) {
const { username, password } = request.body || {}; const { username, password } = request.body || {};
if (!username || !password) { if (!username || !password) {
return reply.status(400).send({ error: '아이디와 비밀번호를 입력해주세요.' }); return reply.code(400).send({ error: '아이디와 비밀번호를 입력해주세요.' });
} }
try { try {
@ -52,14 +52,14 @@ export default async function authRoutes(fastify, opts) {
); );
if (users.length === 0) { if (users.length === 0) {
return reply.status(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }); return reply.code(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
} }
const user = users[0]; const user = users[0];
const isValidPassword = await bcrypt.compare(password, user.password_hash); const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) { if (!isValidPassword) {
return reply.status(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' }); return reply.code(401).send({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
} }
// JWT 토큰 생성 // JWT 토큰 생성
@ -75,7 +75,7 @@ export default async function authRoutes(fastify, opts) {
}; };
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.status(500).send({ error: '로그인 처리 중 오류가 발생했습니다.' }); return reply.code(500).send({ error: '로그인 처리 중 오류가 발생했습니다.' });
} }
}); });

View file

@ -53,7 +53,7 @@ export default async function membersRoutes(fastify, opts) {
return result; return result;
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.status(500).send({ error: '멤버 목록 조회 실패' }); return reply.code(500).send({ error: '멤버 목록 조회 실패' });
} }
}); });
@ -88,7 +88,7 @@ export default async function membersRoutes(fastify, opts) {
`, [decodeURIComponent(name)]); `, [decodeURIComponent(name)]);
if (members.length === 0) { if (members.length === 0) {
return reply.status(404).send({ error: '멤버를 찾을 수 없습니다' }); return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' });
} }
const member = members[0]; const member = members[0];
@ -106,7 +106,7 @@ export default async function membersRoutes(fastify, opts) {
}; };
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.status(500).send({ error: '멤버 조회 실패' }); return reply.code(500).send({ error: '멤버 조회 실패' });
} }
}); });
@ -140,7 +140,7 @@ export default async function membersRoutes(fastify, opts) {
); );
if (existing.length === 0) { if (existing.length === 0) {
return reply.status(404).send({ error: '멤버를 찾을 수 없습니다' }); return reply.code(404).send({ error: '멤버를 찾을 수 없습니다' });
} }
const memberId = existing[0].id; const memberId = existing[0].id;
@ -218,7 +218,7 @@ export default async function membersRoutes(fastify, opts) {
return { message: '멤버 정보가 수정되었습니다', id: memberId }; return { message: '멤버 정보가 수정되었습니다', id: memberId };
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.status(500).send({ error: '멤버 수정 실패: ' + err.message }); return reply.code(500).send({ error: '멤버 수정 실패: ' + err.message });
} }
}); });
} }

View file

@ -70,7 +70,7 @@ export default async function statsRoutes(fastify, opts) {
}; };
} catch (err) { } catch (err) {
fastify.log.error(err); fastify.log.error(err);
return reply.status(500).send({ error: '통계 조회 실패' }); return reply.code(500).send({ error: '통계 조회 실패' });
} }
}); });
} }

View file

@ -0,0 +1,50 @@
/**
* 에러 응답 유틸리티
* 일관된 에러 응답 형식 제공
*/
/**
* 에러 응답 전송
* @param {object} reply - Fastify reply 객체
* @param {number} statusCode - HTTP 상태 코드
* @param {string} message - 에러 메시지
* @returns {object} 에러 응답
*/
export function sendError(reply, statusCode, message) {
return reply.code(statusCode).send({ error: message });
}
/**
* 400 Bad Request
*/
export function badRequest(reply, message = '잘못된 요청입니다.') {
return sendError(reply, 400, message);
}
/**
* 401 Unauthorized
*/
export function unauthorized(reply, message = '인증이 필요합니다.') {
return sendError(reply, 401, message);
}
/**
* 404 Not Found
*/
export function notFound(reply, message = '리소스를 찾을 수 없습니다.') {
return sendError(reply, 404, message);
}
/**
* 409 Conflict
*/
export function conflict(reply, message = '이미 존재하는 리소스입니다.') {
return sendError(reply, 409, message);
}
/**
* 500 Internal Server Error
*/
export function serverError(reply, message = '서버 오류가 발생했습니다.') {
return sendError(reply, 500, message);
}

View file

@ -43,13 +43,18 @@
--- ---
### 4단계: 에러 처리 통일 ### 4단계: 에러 처리 통일 ✅ 완료
- [ ] 에러 응답 유틸리티 생성 (`src/utils/error.js`) - [x] 에러 응답 유틸리티 생성 (`src/utils/error.js`)
- [ ] `reply.status()` vs `reply.code()` 통일 - [x] `reply.status()` `reply.code()` 통일
- [ ] `console.error``fastify.log.error` 변경 - [ ] `console.error``fastify.log.error` 변경 (추후)
**관련 파일:** **생성된 파일:**
- 모든 라우트 파일 - `src/utils/error.js` - sendError, badRequest, unauthorized, notFound, conflict, serverError
**수정된 파일:**
- `src/routes/auth.js` - reply.status → reply.code
- `src/routes/stats/index.js` - reply.status → reply.code
- `src/routes/members/index.js` - reply.status → reply.code
--- ---
@ -72,7 +77,7 @@
| 1단계 | 설정 통합 | ✅ 완료 | | 1단계 | 설정 통합 | ✅ 완료 |
| 2단계 | N+1 쿼리 최적화 | ✅ 완료 | | 2단계 | N+1 쿼리 최적화 | ✅ 완료 |
| 3단계 | 서비스 레이어 분리 | ✅ 완료 | | 3단계 | 서비스 레이어 분리 | ✅ 완료 |
| 4단계 | 에러 처리 통일 | 대기 | | 4단계 | 에러 처리 통일 | ✅ 완료 |
| 5단계 | 중복 코드 제거 | 대기 | | 5단계 | 중복 코드 제거 | 대기 |
--- ---