Repository: jarvisRyu/group-newsfeed
Pull request #15: Feature/exception
Author: login: jarvisRyu
Created: 2025-04-09T05:27:19Z | Merged: 2025-04-09T05:57:38Z



Diff:
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/comment/controller/CommentController.java b/src/main/java/com/cordingrecipe/groupnewsfeed/comment/controller/CommentController.java
index 77608e3..66f83a2 100644
--- a/src/main/java/com/cordingrecipe/groupnewsfeed/comment/controller/CommentController.java
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/comment/controller/CommentController.java
@@ -2,6 +2,7 @@
 
 import com.cordingrecipe.groupnewsfeed.comment.dto.*;
 import com.cordingrecipe.groupnewsfeed.comment.service.CommentService;
+import com.cordingrecipe.groupnewsfeed.user.dto.UserLoginResponseDto;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/comment/service/CommentService.java b/src/main/java/com/cordingrecipe/groupnewsfeed/comment/service/CommentService.java
index 23ad1c5..1c09238 100644
--- a/src/main/java/com/cordingrecipe/groupnewsfeed/comment/service/CommentService.java
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/comment/service/CommentService.java
@@ -6,6 +6,7 @@
 import com.cordingrecipe.groupnewsfeed.comment.entity.Comment;
 import com.cordingrecipe.groupnewsfeed.comment.repository.CommentRepository;
 import com.cordingrecipe.groupnewsfeed.post.entity.Post;
+import com.cordingrecipe.groupnewsfeed.post.repository.PostRepository;
 import com.cordingrecipe.groupnewsfeed.user.entity.User;
 import com.cordingrecipe.groupnewsfeed.user.repository.UserRepository;
 import org.springframework.transaction.annotation.Transactional;
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/CustomException.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/CustomException.java
new file mode 100644
index 0000000..aa9f54e
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/CustomException.java
@@ -0,0 +1,13 @@
+package com.cordingrecipe.groupnewsfeed.common.advice;
+import lombok.Getter;
+
+@Getter
+public class CustomException extends RuntimeException {
+
+    private final ErrorCode errorCode;
+
+    public CustomException(ErrorCode errorCode ) {
+        super(errorCode.getMessage());
+        this.errorCode = errorCode;
+    }
+}
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java
new file mode 100644
index 0000000..9f266c3
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java
@@ -0,0 +1,28 @@
+package com.cordingrecipe.groupnewsfeed.common.advice;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum ErrorCode {
+
+    USER_NOT_FOUND(404,"Not Found","해당 사용자를 찾을 수 없습니다."),
+    USER_EMAIL_DUPLICATED(409,"Conflict","이미 사용 중인 이메일입니다."),
+    USER_UNAUTHORIZED(401, "Unauthorized", "로그인이 필요합니다."),
+    USER_PASSWORD_INVALID(400, "Bad Request", "비밀번호가 올바르지 않습니다."),
+    USER_ALREADY_DELETED(410, "Gone", "이미 탈퇴한 사용자입니다."),
+
+    POST_NOT_FOUND(404, "Not Found", "해당 게시글을 찾을 수 없습니다."),
+    POST_TITLE_REQUIRED(400, "Bad Request", "게시글 제목은 필수입니다."),
+    POST_CONTENT_REQUIRED(400, "Bad Request", "게시글 내용은 필수입니다."),
+    POST_ACCESS_DENIED(403, "Forbidden", "해당 게시글에 대한 접근 권한이 없습니다."),
+
+    COMMENT_CONTENT_REQUIRED(400, "Bad Request", "댓글 내용은 필수입니다."),
+    COMMENT_ACCESS_DENIED(403, "Forbidden", "해당 댓글에 대한 권한이 없습니다.");
+
+    private final int status;
+    private final String error;
+    private final String message;
+
+}
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorResponse.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorResponse.java
new file mode 100644
index 0000000..987036e
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorResponse.java
@@ -0,0 +1,27 @@
+package com.cordingrecipe.groupnewsfeed.common.advice;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)//null 값은 Json에 포함시키지 않음.
+public class ErrorResponse {
+
+    private final LocalDateTime timestamp = LocalDateTime.now();
+    private final int status; //상태코드
+    private final String error; //상태코드 메세지
+    private final String message;
+
+    public ErrorResponse(ErrorCode errorCode) {
+        this.status = errorCode.getStatus();
+        this.error = errorCode.getError();
+        this.message = errorCode.getMessage();
+    }
+
+}
+
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/GlobalExceptionHandler.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/GlobalExceptionHandler.java
new file mode 100644
index 0000000..73f3aaa
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/GlobalExceptionHandler.java
@@ -0,0 +1,101 @@
+package com.cordingrecipe.groupnewsfeed.common.advice;
+
+import lombok.Builder;;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.server.ResponseStatusException;
+import java.nio.file.AccessDeniedException;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    /* 양식
+     * {
+     * "timestamp":"2025-04-08 00:00:00",
+     *  "status" : 400
+     * "error": "Bad_Request",
+     * "message":"XX 에러발생",
+     */
+
+    //Custom Exception 처리
+    @ExceptionHandler(CustomException.class)
+    protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
+        ErrorCode errorCode = e.getErrorCode();
+        ErrorResponse errorResponse = new ErrorResponse(errorCode);
+        return ResponseEntity.status(errorCode.getStatus()).body(errorResponse);
+        //ResponseEntity.status=HTTP 응답 헤더 상태코드
+        //.body = 응답바디(Json)
+    }
+
+    //Valid 예외처리
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+        FieldError fieldError = e.getBindingResult().getFieldError();//메세지없으면 예외발생할수있음
+        String errorMessage = (fieldError != null && fieldError.getDefaultMessage() != null)
+                ? fieldError.getDefaultMessage() : "잘못된 요청입니다.";
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(e.getStatusCode().value())
+                .error("Validation Failed")
+                .message(errorMessage)
+                .build();
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
+    }
+
+    //NPE 예외처리
+    @ExceptionHandler(NullPointerException.class)
+    protected ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(400)
+                .error("Null Pointer Exception")
+                .message(e.getMessage() != null ? e.getMessage() : "처리 중 null 값이 참조되었습니다.")
+                .build();
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
+    }
+
+    @ExceptionHandler(IllegalArgumentException.class)
+    protected ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(400)
+                .error("Illegal Argument")
+                .message(e.getMessage() != null ? e.getMessage() : "잘못된 요청입니다.")
+                .build();
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
+    }
+
+    @ExceptionHandler(AccessDeniedException.class)
+    protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e){
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(403)
+                .error("Access Denied")
+                .message(e.getMessage() != null ? e.getMessage() : "접근 권한이 없습니다.")
+                .build();
+        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
+    }
+
+    @ExceptionHandler(ResponseStatusException.class)
+    protected ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException e){
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(e.getStatusCode().value())
+                .error("Response Status Error")
+                .message(e.getReason() != null ? e.getReason() : "에러가 발생했습니다.")
+                .build();
+        return ResponseEntity.status(e.getStatusCode()).body(errorResponse);
+    }
+
+    //모든 예외처리
+    @ExceptionHandler(Exception.class)
+    protected ResponseEntity<ErrorResponse> handleException(Exception e) {
+        ErrorResponse errorResponse = ErrorResponse.builder()
+                .status(500)
+                .error("Internal Server Error")
+                .message(e.getMessage() != null ? e.getMessage() : "예기치 못한 오류가 발생했습니다.")
+                .build();
+        return ResponseEntity.status(500).body(errorResponse);
+    }
+
+
+}
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/Const.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/Const.java
new file mode 100644
index 0000000..c2bc53e
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/Const.java
@@ -0,0 +1,6 @@
+package com.cordingrecipe.groupnewsfeed.common.filter;
+
+public abstract class Const {
+
+    public static final String LOGIN_USER = "LOGIN_USER";
+}
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/LoginFilter.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/LoginFilter.java
new file mode 100644
index 0000000..cd5d5a2
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/LoginFilter.java
@@ -0,0 +1,45 @@
+package com.cordingrecipe.groupnewsfeed.common.filter;
+
+import jakarta.servlet.*;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.PatternMatchUtils;
+
+import java.io.IOException;
+
+
+
+@Slf4j
+public class LoginFilter implements Filter {
+
+    private static final String[] WHITE_LIST = {"/", "/users/signup", "/users/login", "/users/logout"};
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        HttpServletRequest httpRequest = (HttpServletRequest) request;//다운캐스팅
+        HttpServletResponse httpResponse = (HttpServletResponse) response;//다운캐스팅
+        String requestURI = httpRequest.getRequestURI();//사용자 요청 URI 가져오기
+
+        if (!isWhiteList(requestURI)) { //리스트 조회
+            HttpSession session = httpRequest.getSession(false);//session 이 존재하는지 확인 , 생성X
+            if (session == null || session.getAttribute("LOGIN_USER") == null) {//세션이없거나 LOGIN_USER 에 정보가 없으면
+                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "로그인이 필요합니다.");   //401 오류발생
+                return;
+            }
+        }
+        chain.doFilter(request, response); //인증성공시 전달
+    }
+    private boolean isWhiteList(String requestURI) {
+        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI); //요청에 입력된 URI 가 WHTIE LIST 에 포함되있는지 확인
+    }
+}
+
+
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/WebConfig.java b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/WebConfig.java
new file mode 100644
index 0000000..bc2748e
--- /dev/null
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/WebConfig.java
@@ -0,0 +1,20 @@
+package com.cordingrecipe.groupnewsfeed.common.filter;
+
+import jakarta.servlet.Filter;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class WebConfig {
+    @Bean
+    public FilterRegistrationBean loginFilter(){
+        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
+        filterRegistrationBean.setFilter(new LoginFilter());
+        filterRegistrationBean.setOrder(1);
+        filterRegistrationBean.addUrlPatterns("/*");
+
+        return filterRegistrationBean;
+    }
+
+}
diff --git a/src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java b/src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
index 3121a29..14597ba 100644
--- a/src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
+++ b/src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
@@ -7,19 +7,22 @@
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
 @RequiredArgsConstructor
 public class UserService {
 
     private final UserRepository userRepository;
+    private final PasswordEncoder passwordEncoder;
 
+    @Transactional
     public UserLoginResponseDto login(@Valid UserLoginRequestDto dto) {
         User user = userRepository.findByEmail(dto.getEmail()).orElseThrow(
-                ()->new IllegalArgumentException ("아이디와 비밀번호를 정확히 입력해 주세요.")
+                ()->new IllegalArgumentException ("해당 이메일이 존재하지 않습니다.")
         );
-        if(!user.getPassword().equals(dto.getPassword())){
-        throw new IllegalArgumentException ("아이디와 비밀번호를 정확히 입력해 주세요");
+        if(passwordEncoder.matches(dto.getPassword(),user.getPassword())){
+        throw new IllegalArgumentException ("비밀번호가 일치하지 않습니다.");
         }
         return new UserLoginResponseDto(
                 user.getId(),


Reviews:
user: login: hosung77
body: 리뷰 남깁니다!
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T05:36:33Z
user: login: jarvisRyu
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T05:48:18Z
user: login: hosung77
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T05:50:31Z
user: login: jarvisRyu
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T05:50:36Z
user: login: hosung77
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T05:51:23Z
user: login: jarvisRyu
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T05:55:40Z

Review comments:
path: src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/LoginFilter.java
user: login: hosung77
body: 명세서에 /api를 사용하기로 하였는데 white_list에 /users 이런식으로 되어져있습니다. /api/users 이런식으로 변경하면 좋을것 같습니다.
created_at: 2025-04-09T05:31:15Z
author_association: COLLABORATOR
path: src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
user: login: hosung77
body: 커스텀 예외처리가 생겼으니 new IllegalAr~ 이 부분을 new customException(Errorcode.EMAIL_NOT_FOUND) 이런식으로 적용해주면 좋을것 같습니다.
created_at: 2025-04-09T05:33:30Z
author_association: COLLABORATOR
path: src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java
user: login: hosung77
body: 댓글 내용이 필수인 이유를 알고싶습니다.! 댓글이 안달린 글들이 있을텐데 어떤 이유에서 만든건지 궁금합니다. 그리고 이미 사용 중인 이메일, 게시글 제목 필수, 게시글 내용 필수 이 부분은 @Valid 부분에서 처리 가능할텐데 굳이 만들어야 될까? 라는 생각이 듭니다!
created_at: 2025-04-09T05:36:16Z
author_association: COLLABORATOR
path: src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java
user: login: jarvisRyu
body: 발생할 수 있는 경우를 나열하다보니 게시글처럼 입력값이 반드시 있어야한다고 혼용한것 같습니다.
삭제예정
created_at: 2025-04-09T05:48:18Z
author_association: OWNER
path: src/main/java/com/cordingrecipe/groupnewsfeed/common/advice/ErrorCode.java
user: login: hosung77
body: 답변 감사드립니다!
created_at: 2025-04-09T05:50:31Z
author_association: COLLABORATOR
path: src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
user: login: jarvisRyu
body: 확인했습니다. 필요한  커스텀에러들 더추가시켜나가면 될것같습니다.
created_at: 2025-04-09T05:50:36Z
author_association: OWNER
path: src/main/java/com/cordingrecipe/groupnewsfeed/user/service/UserService.java
user: login: hosung77
body: 넵!
created_at: 2025-04-09T05:51:23Z
author_association: COLLABORATOR
path: src/main/java/com/cordingrecipe/groupnewsfeed/common/filter/LoginFilter.java
user: login: jarvisRyu
body: api/users .. 양식 적용
created_at: 2025-04-09T05:55:40Z
author_association: OWNER

PR comments:
user: login: EZRANDO
created_at: 2025-04-09T05:30:24Z
body: 확인했습니다

author_association: COLLABORATOR
user: login: HanGil-Jeong
created_at: 2025-04-09T05:32:30Z
body: 확인했습니다.
author_association: COLLABORATOR