diff --git a/src/main/java/econo/buddybridge/auth/dto/OAuthInfoResponse.java b/src/main/java/econo/buddybridge/auth/dto/OAuthInfoResponse.java index cc645c2..4fa774c 100644 --- a/src/main/java/econo/buddybridge/auth/dto/OAuthInfoResponse.java +++ b/src/main/java/econo/buddybridge/auth/dto/OAuthInfoResponse.java @@ -1,5 +1,9 @@ package econo.buddybridge.auth.dto; +import econo.buddybridge.member.entity.DisabilityType; +import econo.buddybridge.member.entity.Gender; +import econo.buddybridge.member.entity.Member; + public interface OAuthInfoResponse { String getEmail(); String getName(); @@ -7,4 +11,16 @@ public interface OAuthInfoResponse { Integer getAge(); String getGender(); String getProfileImageUrl(); + + default Member toMember() { + return Member.builder() + .email(getEmail()) + .name(getName()) + .nickname(getNickname()) + .age(getAge()) + .gender(Gender.fromEnglishName(getGender())) + .profileImageUrl(getProfileImageUrl()) + .disabilityType(DisabilityType.없음) // 회원가입 시 초기 장애 유형은 없음으로 설정 + .build(); + } } diff --git a/src/main/java/econo/buddybridge/auth/service/OAuthInfoService.java b/src/main/java/econo/buddybridge/auth/service/OAuthInfoService.java index b6caf49..d70025f 100644 --- a/src/main/java/econo/buddybridge/auth/service/OAuthInfoService.java +++ b/src/main/java/econo/buddybridge/auth/service/OAuthInfoService.java @@ -4,6 +4,7 @@ import econo.buddybridge.auth.client.OAuthApiClient; import econo.buddybridge.auth.dto.OAuthInfoResponse; import econo.buddybridge.auth.dto.OAuthLoginParams; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -18,9 +19,8 @@ public class OAuthInfoService { private final Map clients; public OAuthInfoService(List clients) { - this.clients = clients.stream().collect( - Collectors.toUnmodifiableMap(OAuthApiClient::getOAuthProvider, Function.identity()) - ); + this.clients = new EnumMap<>(clients.stream() + .collect(Collectors.toMap(OAuthApiClient::getOAuthProvider, Function.identity()))); } public OAuthInfoResponse getUserInfo(OAuthLoginParams params) { diff --git a/src/main/java/econo/buddybridge/chat/chatmessage/service/ChatMessageService.java b/src/main/java/econo/buddybridge/chat/chatmessage/service/ChatMessageService.java index 7bd4693..686209f 100644 --- a/src/main/java/econo/buddybridge/chat/chatmessage/service/ChatMessageService.java +++ b/src/main/java/econo/buddybridge/chat/chatmessage/service/ChatMessageService.java @@ -7,29 +7,27 @@ import econo.buddybridge.matching.entity.Matching; import econo.buddybridge.matching.repository.MatchingRepository; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.notification.entity.NotificationType; import econo.buddybridge.notification.service.EmitterService; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor public class ChatMessageService { - private final MemberRepository memberRepository; + private final MemberService memberService; private final MatchingRepository matchingRepository; private final ChatMessageRepository chatMessageRepository; private final EmitterService emitterService; @Transactional // 메시지 저장 public ChatMessageResDto save(Long senderId, ChatMessageReqDto chatMessageReqDto, Long matchingId) { - Member sender = memberRepository.findById(senderId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + Member sender = memberService.findMemberByIdOrThrow(senderId); Matching matching = matchingRepository.findById(matchingId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 매칭입니다.")); @@ -42,8 +40,7 @@ public ChatMessageResDto save(Long senderId, ChatMessageReqDto chatMessageReqDto .build(); Long receiverId = getReceiverId(sender.getId(), matching.getId()); - Member receiver = memberRepository.findById(receiverId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + Member receiver = memberService.findMemberByIdOrThrow(receiverId); emitterService.send( // 채팅을 받는 사용자에게 알림 전송 receiver, diff --git a/src/main/java/econo/buddybridge/comment/exception/CommentDeleteNotAllowedException.java b/src/main/java/econo/buddybridge/comment/exception/CommentDeleteNotAllowedException.java new file mode 100644 index 0000000..da65b79 --- /dev/null +++ b/src/main/java/econo/buddybridge/comment/exception/CommentDeleteNotAllowedException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.comment.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class CommentDeleteNotAllowedException extends BusinessException { + + public static BusinessException EXCEPTION = new CommentDeleteNotAllowedException(); + + private CommentDeleteNotAllowedException() { + super(CommentErrorCode.COMMENT_DELETE_NOT_ALLOWED); + } + +} diff --git a/src/main/java/econo/buddybridge/comment/exception/CommentErrorCode.java b/src/main/java/econo/buddybridge/comment/exception/CommentErrorCode.java new file mode 100644 index 0000000..5307f18 --- /dev/null +++ b/src/main/java/econo/buddybridge/comment/exception/CommentErrorCode.java @@ -0,0 +1,37 @@ +package econo.buddybridge.comment.exception; + +import econo.buddybridge.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum CommentErrorCode implements ErrorCode { + COMMENT_NOT_FOUND("C001", HttpStatus.NOT_FOUND, "존재하지 않는 댓글입니다."), + COMMENT_UPDATE_NOT_ALLOWED("C002", HttpStatus.FORBIDDEN, "본인의 댓글만 수정할 수 있습니다."), + COMMENT_DELETE_NOT_ALLOWED("C003", HttpStatus.FORBIDDEN, "본인의 댓글만 삭제할 수 있습니다."), + COMMENT_INVALID_DIRECTION("C004", HttpStatus.BAD_REQUEST, "올바르지 않은 정렬 방식입니다."), + ; + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + CommentErrorCode(String code, HttpStatus httpStatus, String message) { + this.code = code; + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/econo/buddybridge/comment/exception/CommentInvalidDirectionException.java b/src/main/java/econo/buddybridge/comment/exception/CommentInvalidDirectionException.java new file mode 100644 index 0000000..e6e64d5 --- /dev/null +++ b/src/main/java/econo/buddybridge/comment/exception/CommentInvalidDirectionException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.comment.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class CommentInvalidDirectionException extends BusinessException { + + public static BusinessException EXCEPTION = new CommentInvalidDirectionException(); + + private CommentInvalidDirectionException() { + super(CommentErrorCode.COMMENT_INVALID_DIRECTION); + } + +} diff --git a/src/main/java/econo/buddybridge/comment/exception/CommentNotFoundException.java b/src/main/java/econo/buddybridge/comment/exception/CommentNotFoundException.java new file mode 100644 index 0000000..b85a486 --- /dev/null +++ b/src/main/java/econo/buddybridge/comment/exception/CommentNotFoundException.java @@ -0,0 +1,12 @@ +package econo.buddybridge.comment.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class CommentNotFoundException extends BusinessException { + + public static BusinessException EXCEPTION = new CommentNotFoundException(); + + private CommentNotFoundException() { + super(CommentErrorCode.COMMENT_NOT_FOUND); + } +} diff --git a/src/main/java/econo/buddybridge/comment/exception/CommentUpdateNotAllowedException.java b/src/main/java/econo/buddybridge/comment/exception/CommentUpdateNotAllowedException.java new file mode 100644 index 0000000..a03e882 --- /dev/null +++ b/src/main/java/econo/buddybridge/comment/exception/CommentUpdateNotAllowedException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.comment.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class CommentUpdateNotAllowedException extends BusinessException { + + public static BusinessException EXCEPTION = new CommentUpdateNotAllowedException(); + + private CommentUpdateNotAllowedException() { + super(CommentErrorCode.COMMENT_UPDATE_NOT_ALLOWED); + } + +} diff --git a/src/main/java/econo/buddybridge/comment/service/CommentService.java b/src/main/java/econo/buddybridge/comment/service/CommentService.java index dda7d94..6d1e32b 100644 --- a/src/main/java/econo/buddybridge/comment/service/CommentService.java +++ b/src/main/java/econo/buddybridge/comment/service/CommentService.java @@ -3,10 +3,14 @@ import econo.buddybridge.comment.dto.CommentCustomPage; import econo.buddybridge.comment.dto.CommentReqDto; import econo.buddybridge.comment.entity.Comment; +import econo.buddybridge.comment.exception.CommentDeleteNotAllowedException; +import econo.buddybridge.comment.exception.CommentInvalidDirectionException; +import econo.buddybridge.comment.exception.CommentNotFoundException; +import econo.buddybridge.comment.exception.CommentUpdateNotAllowedException; import econo.buddybridge.comment.repository.CommentRepository; import econo.buddybridge.comment.repository.CommentRepositoryCustom; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.notification.entity.NotificationType; import econo.buddybridge.notification.service.EmitterService; import econo.buddybridge.post.entity.Post; @@ -23,7 +27,7 @@ @RequiredArgsConstructor public class CommentService { - private final MemberRepository memberRepository; + private final MemberService memberService; private final PostRepository postRepository; private final CommentRepository commentRepository; private final CommentRepositoryCustom commentRepositoryCustom; @@ -31,40 +35,47 @@ public class CommentService { @Transactional(readOnly = true) // 댓글 조회 public CommentCustomPage getComments(Long postId, Integer size, String order, Long cursor) { - Post post = postRepository.findById(postId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); + Post post = findPostByIdOrThrow(postId); Direction direction; try { direction = Direction.valueOf(order); } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("올바르지 않은 정렬 방식입니다."); + throw CommentInvalidDirectionException.EXCEPTION; } PageRequest page = PageRequest.of(0, size, Sort.by(direction, "id")); return commentRepositoryCustom.findByPost(post, cursor, page); } + + private Post findPostByIdOrThrow(Long postId) { + return postRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); + } @Transactional // 댓글 생성 public Long createComment(CommentReqDto commentReqDto, Long postId, Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + Member member = memberService.findMemberByIdOrThrow(memberId); - Post post = postRepository.findById(postId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); + Post post = findPostByIdOrThrow(postId); Comment comment = commentReqToComment(commentReqDto, post, member); + // 게시글 작성자에게 댓글 알림 전송 + sendNotificationToPostAuthor(member, comment, post); + + return commentRepository.save(comment).getId(); + } + + private void sendNotificationToPostAuthor(Member member, Comment comment, Post post) { // 알림 내용은 댓글 작성자 이름과 댓글 내용 String notificationContent = member.getName() + "님이 댓글을 남겼습니다. - " + comment.getContent(); String notificationUrl = getCommentNotificationUrl(post.getPostType(), post.getId()); // 댓글 알림은 게시글 작성자에게 전송 emitterService.send(post.getAuthor(), notificationContent, notificationUrl, NotificationType.COMMENT); - - return commentRepository.save(comment).getId(); } private String getCommentNotificationUrl(PostType postType, Long postId) { @@ -81,11 +92,10 @@ private String getCommentNotificationUrl(PostType postType, Long postId) { @Transactional // 댓글 수정 public Long updateComment(Long commentId, CommentReqDto commentReqDto, Long memberId) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다.")); + Comment comment = findCommentByIdOrThrow(commentId); if (!comment.getAuthor().getId().equals(memberId)) { - throw new IllegalArgumentException("본인의 댓글만 수정할 수 있습니다."); + throw CommentUpdateNotAllowedException.EXCEPTION; } comment.updateContent(commentReqDto.content()); @@ -95,16 +105,20 @@ public Long updateComment(Long commentId, CommentReqDto commentReqDto, Long memb @Transactional // 댓글 삭제 public void deleteComment(Long commentId, Long memberId) { - Comment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다.")); + Comment comment = findCommentByIdOrThrow(commentId); if (!comment.getAuthor().getId().equals(memberId)) { - throw new IllegalArgumentException("본인의 댓글만 삭제할 수 있습니다."); + throw CommentDeleteNotAllowedException.EXCEPTION; } commentRepository.delete(comment); } + private Comment findCommentByIdOrThrow(Long commentId) { + return commentRepository.findById(commentId) + .orElseThrow(() -> CommentNotFoundException.EXCEPTION); + } + private Comment commentReqToComment(CommentReqDto commentReqDto, Post post, Member member) { return Comment.builder() .post(post) diff --git a/src/main/java/econo/buddybridge/common/exception/BusinessException.java b/src/main/java/econo/buddybridge/common/exception/BusinessException.java new file mode 100644 index 0000000..8acf507 --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/BusinessException.java @@ -0,0 +1,15 @@ +package econo.buddybridge.common.exception; + +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/econo/buddybridge/common/exception/CommonErrorCode.java b/src/main/java/econo/buddybridge/common/exception/CommonErrorCode.java new file mode 100644 index 0000000..ee32e62 --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/CommonErrorCode.java @@ -0,0 +1,33 @@ +package econo.buddybridge.common.exception; + +import org.springframework.http.HttpStatus; + +public enum CommonErrorCode implements ErrorCode { + INVALID_INPUT_VALUE("C001", HttpStatus.BAD_REQUEST, "요청 값이 잘못되었습니다."), + ; + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + CommonErrorCode(String code, HttpStatus httpStatus, String message) { + this.code = code; + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/econo/buddybridge/common/exception/ErrorCode.java b/src/main/java/econo/buddybridge/common/exception/ErrorCode.java new file mode 100644 index 0000000..fa4ea25 --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/ErrorCode.java @@ -0,0 +1,10 @@ +package econo.buddybridge.common.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getCode(); + HttpStatus getHttpStatus(); + String getMessage(); +} diff --git a/src/main/java/econo/buddybridge/common/exception/ErrorResponse.java b/src/main/java/econo/buddybridge/common/exception/ErrorResponse.java new file mode 100644 index 0000000..980716a --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/ErrorResponse.java @@ -0,0 +1,24 @@ +package econo.buddybridge.common.exception; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class ErrorResponse { + + private final String code; + private final int status; + private final String message; + + @Setter + @JsonInclude(JsonInclude.Include.NON_NULL) + private List invalidParams; + + public ErrorResponse(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.status = errorCode.getHttpStatus().value(); + this.message = errorCode.getMessage(); + } +} diff --git a/src/main/java/econo/buddybridge/common/exception/GlobalExceptionHandler.java b/src/main/java/econo/buddybridge/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b9ad838 --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,52 @@ +package econo.buddybridge.common.exception; + +import econo.buddybridge.utils.api.ApiResponse; +import econo.buddybridge.utils.api.ApiResponse.CustomBody; +import econo.buddybridge.utils.api.ApiResponseGenerator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ApiResponse> handleBusinessException(BusinessException ex) { + ErrorCode errorCode = ex.getErrorCode(); + + return ApiResponseGenerator.fail(new ErrorResponse(errorCode), errorCode.getHttpStatus()); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( // 파라미터 유효성 검사 실패 시 발생하는 예외 처리 + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + // 공통 에러 코드로 처리 + // 세부 검증 오류는 ErrorResponse.invalidParams에 담아서 반환 + ErrorCode errorCode = CommonErrorCode.INVALID_INPUT_VALUE; + + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(makeErrorResponse(ex, errorCode)); + } + + private ErrorResponse makeErrorResponse(MethodArgumentNotValidException ex, ErrorCode errorCode) { + ErrorResponse errorResponse = new ErrorResponse(errorCode); + + errorResponse.setInvalidParams(ex.getBindingResult() + .getFieldErrors() + .stream() + .map(ValidationError::of) + .toList()); + + return errorResponse; + } + +} diff --git a/src/main/java/econo/buddybridge/common/exception/ValidationError.java b/src/main/java/econo/buddybridge/common/exception/ValidationError.java new file mode 100644 index 0000000..91379b9 --- /dev/null +++ b/src/main/java/econo/buddybridge/common/exception/ValidationError.java @@ -0,0 +1,12 @@ +package econo.buddybridge.common.exception; + +import org.springframework.validation.FieldError; + +public record ValidationError( + String field, + String message +) { + public static ValidationError of(final FieldError fieldError) { + return new ValidationError(fieldError.getField(), fieldError.getDefaultMessage()); + } +} diff --git a/src/main/java/econo/buddybridge/config/SessionInterceptor.java b/src/main/java/econo/buddybridge/config/SessionInterceptor.java index b8bf57d..8944bde 100644 --- a/src/main/java/econo/buddybridge/config/SessionInterceptor.java +++ b/src/main/java/econo/buddybridge/config/SessionInterceptor.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -29,11 +30,10 @@ public boolean preHandle( } } - String requestMethod = request.getMethod(); - if (requestMethod.equals("OPTIONS")) { // CORS preflight 요청 처리 + if (CorsUtils.isPreFlightRequest(request)) { // CORS preflight 요청은 세션 검증을 하지 않음 return true; } return sessionValidator.validate(request, response); } -} \ No newline at end of file +} diff --git a/src/main/java/econo/buddybridge/matching/service/MatchingRoomService.java b/src/main/java/econo/buddybridge/matching/service/MatchingRoomService.java index d4ae24f..4e0380f 100644 --- a/src/main/java/econo/buddybridge/matching/service/MatchingRoomService.java +++ b/src/main/java/econo/buddybridge/matching/service/MatchingRoomService.java @@ -11,8 +11,10 @@ import econo.buddybridge.matching.repository.MatchingRepository; import econo.buddybridge.matching.repository.MatchingRepositoryCustom; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.post.entity.Post; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -20,14 +22,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; - @Service @RequiredArgsConstructor public class MatchingRoomService { - private final MemberRepository memberRepository; + private final MemberService memberService; private final MatchingRepository matchingRepository; private final ChatMessageRepository chatMessageRepository; private final MatchingRepositoryCustom matchingRepositoryCustom; @@ -93,7 +92,6 @@ private Member getReceiver(Matching matching, Long memberId) { receiverId = matching.getGiver().getId(); } - return memberRepository.findById(receiverId) - .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + return memberService.findMemberByIdOrThrow(receiverId); } } diff --git a/src/main/java/econo/buddybridge/matching/service/MatchingService.java b/src/main/java/econo/buddybridge/matching/service/MatchingService.java index 2b96ca0..01aeb1c 100644 --- a/src/main/java/econo/buddybridge/matching/service/MatchingService.java +++ b/src/main/java/econo/buddybridge/matching/service/MatchingService.java @@ -9,7 +9,7 @@ import econo.buddybridge.matching.entity.MatchingStatus; import econo.buddybridge.matching.repository.MatchingRepository; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.post.entity.Post; import econo.buddybridge.post.entity.PostType; import econo.buddybridge.post.repository.PostRepository; @@ -23,25 +23,22 @@ public class MatchingService { private final ChatMessageRepository chatMessageRepository; private final MatchingRepository matchingRepository; private final PostRepository postRepository; - private final MemberRepository memberRepository; + private final MemberService memberService; @Transactional // TODO: 매칭 생성 -> 예외처리 필요 + 댓글에서 사용자 정보 가져오기 고려 public Long createMatchingById(MatchingReqDto matchingReqDto, Long memberId) { Post post = postRepository.findById(matchingReqDto.postId()) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); - Member loginMember = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + Member loginMember = memberService.findMemberByIdOrThrow(memberId); Member taker, giver; if (post.getPostType() == PostType.GIVER) { giver = loginMember; - taker = memberRepository.findById(matchingReqDto.takerId()) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + taker = memberService.findMemberByIdOrThrow(matchingReqDto.takerId()); } else { - giver = memberRepository.findById(matchingReqDto.giverId()) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + giver = memberService.findMemberByIdOrThrow(matchingReqDto.giverId()); taker = loginMember; } diff --git a/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java b/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java new file mode 100644 index 0000000..d385438 --- /dev/null +++ b/src/main/java/econo/buddybridge/member/exception/MemberErrorCode.java @@ -0,0 +1,34 @@ +package econo.buddybridge.member.exception; + +import econo.buddybridge.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum MemberErrorCode implements ErrorCode { + MEMBER_NOT_FOUND("M001", HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), + ; + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + MemberErrorCode(String code, HttpStatus httpStatus, String message) { + this.code = code; + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/econo/buddybridge/member/exception/MemberNotFoundException.java b/src/main/java/econo/buddybridge/member/exception/MemberNotFoundException.java new file mode 100644 index 0000000..bb01c94 --- /dev/null +++ b/src/main/java/econo/buddybridge/member/exception/MemberNotFoundException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.member.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class MemberNotFoundException extends BusinessException { + + public static BusinessException EXCEPTION = new MemberNotFoundException(); + + private MemberNotFoundException() { + super(MemberErrorCode.MEMBER_NOT_FOUND); + } + +} diff --git a/src/main/java/econo/buddybridge/member/service/MemberService.java b/src/main/java/econo/buddybridge/member/service/MemberService.java index 7bfe6ba..47167a5 100644 --- a/src/main/java/econo/buddybridge/member/service/MemberService.java +++ b/src/main/java/econo/buddybridge/member/service/MemberService.java @@ -3,16 +3,13 @@ import econo.buddybridge.auth.dto.OAuthInfoResponse; import econo.buddybridge.member.dto.MemberReqDto; import econo.buddybridge.member.dto.MemberResDto; -import econo.buddybridge.member.entity.DisabilityType; -import econo.buddybridge.member.entity.Gender; import econo.buddybridge.member.entity.Member; +import econo.buddybridge.member.exception.MemberNotFoundException; import econo.buddybridge.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor public class MemberService { @@ -21,17 +18,8 @@ public class MemberService { @Transactional(readOnly = true) public MemberResDto findMemberById(Long memberId) { - Member member = validateVerifyMemberById(memberId); - return MemberResDto.builder() - .memberId(member.getId()) - .name(member.getName()) - .nickname(member.getNickname()) - .email(member.getEmail()) - .age(member.getAge()) - .gender(member.getGender()) - .disabilityType(member.getDisabilityType()) - .profileImageUrl(member.getProfileImageUrl()) - .build(); + Member member = findMemberByIdOrThrow(memberId); + return new MemberResDto(member); } @Transactional(readOnly = true) @@ -40,44 +28,27 @@ public boolean existsById(Long memberId) { } // 존재하는 회원인지 확인 - public Member validateVerifyMemberById(Long memberId) { - Optional optionalMember = memberRepository.findById(memberId); - return optionalMember.orElseThrow( - () -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + @Transactional(readOnly = true) + public Member findMemberByIdOrThrow(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> MemberNotFoundException.EXCEPTION); } @Transactional public MemberResDto findOrCreateMemberByEmail(OAuthInfoResponse info) { Member member = memberRepository.findByEmail(info.getEmail()) .orElseGet(() -> newMember(info)); - return MemberResDto.builder() - .memberId(member.getId()) - .name(member.getName()) - .nickname(member.getNickname()) - .email(member.getEmail()) - .age(member.getAge()) - .gender(member.getGender()) - .disabilityType(member.getDisabilityType()) - .profileImageUrl(member.getProfileImageUrl()) - .build(); + return new MemberResDto(member); } private Member newMember(OAuthInfoResponse info) { - Member member = Member.builder() - .email(info.getEmail()) - .name(info.getName()) - .nickname(info.getNickname()) - .age(info.getAge()) - .gender(Gender.fromEnglishName(info.getGender())) - .disabilityType(DisabilityType.없음) - .profileImageUrl(info.getProfileImageUrl()) - .build(); + Member member = info.toMember(); return memberRepository.save(member); } @Transactional public void updateMemberById(Long memberId, MemberReqDto memberReqDto) { - Member member = validateVerifyMemberById(memberId); + Member member = findMemberByIdOrThrow(memberId); member.updateMemberInfo(memberReqDto.name(), memberReqDto.nickname(), memberReqDto.profileImageUrl(), memberReqDto.email(), memberReqDto.age(), memberReqDto.disabilityType(), member.getGender()); diff --git a/src/main/java/econo/buddybridge/notification/controller/NotificationController.java b/src/main/java/econo/buddybridge/notification/controller/NotificationController.java index d668e13..bd94854 100644 --- a/src/main/java/econo/buddybridge/notification/controller/NotificationController.java +++ b/src/main/java/econo/buddybridge/notification/controller/NotificationController.java @@ -7,6 +7,7 @@ import econo.buddybridge.utils.api.ApiResponseGenerator; import econo.buddybridge.utils.session.SessionUtils; import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -16,15 +17,12 @@ import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor @RequestMapping("/api/notifications") public class NotificationController { private final NotificationService notificationService; - public NotificationController(NotificationService notificationService) { - this.notificationService = notificationService; - } - @GetMapping public ApiResponse> getNotifications( @RequestParam("limit") Integer size, diff --git a/src/main/java/econo/buddybridge/notification/exception/NotificationAccessDeniedException.java b/src/main/java/econo/buddybridge/notification/exception/NotificationAccessDeniedException.java new file mode 100644 index 0000000..158b619 --- /dev/null +++ b/src/main/java/econo/buddybridge/notification/exception/NotificationAccessDeniedException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.notification.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class NotificationAccessDeniedException extends BusinessException { + + public static BusinessException EXCEPTION = new NotificationAccessDeniedException(); + + public NotificationAccessDeniedException() { + super(NotificationErrorCode.NOTIFICATION_ACCESS_DENIED); + } + +} diff --git a/src/main/java/econo/buddybridge/notification/exception/NotificationErrorCode.java b/src/main/java/econo/buddybridge/notification/exception/NotificationErrorCode.java new file mode 100644 index 0000000..ef4a368 --- /dev/null +++ b/src/main/java/econo/buddybridge/notification/exception/NotificationErrorCode.java @@ -0,0 +1,35 @@ +package econo.buddybridge.notification.exception; + +import econo.buddybridge.common.exception.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum NotificationErrorCode implements ErrorCode { + NOTIFICATION_NOT_FOUND("N001", HttpStatus.NOT_FOUND, "존재하지 않는 알림입니다."), + NOTIFICATION_ACCESS_DENIED("N002", HttpStatus.FORBIDDEN, "본인의 알림만 읽을 수 있습니다.") + ; + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + NotificationErrorCode(String code, HttpStatus httpStatus, String message) { + this.code = code; + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/econo/buddybridge/notification/exception/NotificationNotFoundException.java b/src/main/java/econo/buddybridge/notification/exception/NotificationNotFoundException.java new file mode 100644 index 0000000..4c738be --- /dev/null +++ b/src/main/java/econo/buddybridge/notification/exception/NotificationNotFoundException.java @@ -0,0 +1,13 @@ +package econo.buddybridge.notification.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class NotificationNotFoundException extends BusinessException { + + public static BusinessException EXCEPTION = new NotificationNotFoundException(); + + public NotificationNotFoundException() { + super(NotificationErrorCode.NOTIFICATION_NOT_FOUND); + } + +} diff --git a/src/main/java/econo/buddybridge/notification/service/EmitterService.java b/src/main/java/econo/buddybridge/notification/service/EmitterService.java index b7939f2..d99c0c7 100644 --- a/src/main/java/econo/buddybridge/notification/service/EmitterService.java +++ b/src/main/java/econo/buddybridge/notification/service/EmitterService.java @@ -23,17 +23,7 @@ public SseEmitter connect(String memberId, String lastEventId) { SseEmitter emitter = emitterRepository.save(eventId, new SseEmitter(CONNECT_TIMEOUT)); - emitter.onCompletion(() -> emitterRepository.deleteById(eventId)); - - emitter.onTimeout(() -> { - emitter.complete(); - emitterRepository.deleteById(eventId); - }); - - emitter.onError((error) -> { - emitter.completeWithError(error); - emitterRepository.deleteById(eventId); - }); + initEmitter(emitter, eventId); // emitter 초기 설정 // 503 에러를 방지하기 위한 더미 데이터 전송 sendNotification(emitter, eventId, "EventStream Created. [memberId=" + memberId + "]"); @@ -50,6 +40,20 @@ private String generateEventIdByMemberId(String memberId) { return memberId + "_" + System.currentTimeMillis(); } + private void initEmitter(SseEmitter emitter, String eventId) { + emitter.onCompletion(() -> emitterRepository.deleteById(eventId)); + + emitter.onTimeout(() -> { + emitter.complete(); + emitterRepository.deleteById(eventId); + }); + + emitter.onError((error) -> { + emitter.completeWithError(error); + emitterRepository.deleteById(eventId); + }); + } + private void sendNotification(SseEmitter emitter, String eventId, Object payload) { try { emitter.send(SseEmitter.event() diff --git a/src/main/java/econo/buddybridge/notification/service/NotificationService.java b/src/main/java/econo/buddybridge/notification/service/NotificationService.java index 72aa373..3a111fe 100644 --- a/src/main/java/econo/buddybridge/notification/service/NotificationService.java +++ b/src/main/java/econo/buddybridge/notification/service/NotificationService.java @@ -1,9 +1,11 @@ package econo.buddybridge.notification.service; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.notification.dto.NotificationCustomPage; import econo.buddybridge.notification.entity.Notification; +import econo.buddybridge.notification.exception.NotificationAccessDeniedException; +import econo.buddybridge.notification.exception.NotificationNotFoundException; import econo.buddybridge.notification.repository.NotificationRepository; import econo.buddybridge.notification.repository.NotificationRepositoryCustom; import lombok.RequiredArgsConstructor; @@ -14,7 +16,7 @@ @RequiredArgsConstructor public class NotificationService { - private final MemberRepository memberRepository; + private final MemberService memberService; private final NotificationRepository notificationRepository; private final NotificationRepositoryCustom notificationRepositoryCustom; @@ -25,21 +27,19 @@ public void saveNotification(Notification notification) { @Transactional(readOnly = true) public NotificationCustomPage getNotifications(Long memberId, Integer size, Long cursor) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + Member member = memberService.findMemberByIdOrThrow(memberId); return notificationRepositoryCustom.findByMemberId(member.getId(), size, cursor); } @Transactional public void markAsRead(Long notificationId, Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + Member member = memberService.findMemberByIdOrThrow(memberId); Notification notification = notificationRepository.findById(notificationId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 알림입니다.")); + .orElseThrow(() -> NotificationNotFoundException.EXCEPTION); if (!notification.getReceiver().getId().equals(member.getId())) { - throw new IllegalArgumentException("본인의 알림만 읽을 수 있습니다."); + throw NotificationAccessDeniedException.EXCEPTION; } notification.markAsRead(); diff --git a/src/main/java/econo/buddybridge/post/service/PostService.java b/src/main/java/econo/buddybridge/post/service/PostService.java index d1178b4..ee271e1 100644 --- a/src/main/java/econo/buddybridge/post/service/PostService.java +++ b/src/main/java/econo/buddybridge/post/service/PostService.java @@ -1,7 +1,7 @@ package econo.buddybridge.post.service; import econo.buddybridge.member.entity.Member; -import econo.buddybridge.member.repository.MemberRepository; +import econo.buddybridge.member.service.MemberService; import econo.buddybridge.post.dto.PostCustomPage; import econo.buddybridge.post.dto.PostReqDto; import econo.buddybridge.post.dto.PostResDto; @@ -21,7 +21,7 @@ public class PostService { private final PostRepository postRepository; private final PostRepositoryCustom postRepositoryCustom; - private final MemberRepository memberRepository; + private final MemberService memberService; @Transactional(readOnly = true) // 단일 게시글 조회 public PostResDto findPost(Long postId) { @@ -38,8 +38,7 @@ public PostCustomPage getPosts(Integer page, Integer size, String sort, PostType // 검증 과정 필요성 고려 @Transactional // 게시글 생성 public Long createPost(PostReqDto postReqDto, Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + Member member = memberService.findMemberByIdOrThrow(memberId); Post post = postReqDto.toEntity(member); return postRepository.save(post).getId(); diff --git a/src/main/java/econo/buddybridge/utils/api/ApiResponse.java b/src/main/java/econo/buddybridge/utils/api/ApiResponse.java index 864833a..7c0de5f 100644 --- a/src/main/java/econo/buddybridge/utils/api/ApiResponse.java +++ b/src/main/java/econo/buddybridge/utils/api/ApiResponse.java @@ -1,5 +1,6 @@ package econo.buddybridge.utils.api; +import econo.buddybridge.common.exception.ErrorResponse; import java.io.Serializable; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,6 +30,6 @@ public ApiResponse(B body, HttpHeaders headers, HttpStatus status) { public static class CustomBody implements Serializable { private Boolean success; private D data; - private Error error; + private ErrorResponse error; } -} \ No newline at end of file +} diff --git a/src/main/java/econo/buddybridge/utils/api/ApiResponseGenerator.java b/src/main/java/econo/buddybridge/utils/api/ApiResponseGenerator.java index 9f6f47e..8d14172 100644 --- a/src/main/java/econo/buddybridge/utils/api/ApiResponseGenerator.java +++ b/src/main/java/econo/buddybridge/utils/api/ApiResponseGenerator.java @@ -1,5 +1,6 @@ package econo.buddybridge.utils.api; +import econo.buddybridge.common.exception.ErrorResponse; import lombok.experimental.UtilityClass; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -20,8 +21,8 @@ public static ApiResponse> success(final D data, f return new ApiResponse<>(new ApiResponse.CustomBody<>(true, data,null), mediaType,status); } - public static ApiResponse> fail(String message, final HttpStatus status) { - return new ApiResponse<>(new ApiResponse.CustomBody<>(false,null, new Error(message, status.toString())), status); + public static ApiResponse> fail(final ErrorResponse errorResponse, final HttpStatus status) { + return new ApiResponse<>(new ApiResponse.CustomBody<>(false, null, errorResponse), status); } public static ApiResponse> success(final D data, final HttpHeaders headers, final HttpStatus status) {