From 839b5b57a2d42a7d775cea1545584fc1b59a96ce Mon Sep 17 00:00:00 2001 From: taehyeon Date: Wed, 25 Jun 2025 17:12:43 +0900 Subject: [PATCH 01/15] Lv-1 --- .../org/example/expert/domain/todo/service/TodoService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 922991ce7..89561fd12 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -19,7 +19,7 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class TodoService { private final TodoRepository todoRepository; From 187edf6e13efadf8c0fcf0668b3e978a6fbd2d33 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Thu, 26 Jun 2025 16:21:31 +0900 Subject: [PATCH 02/15] =?UTF-8?q?bug=20:=201=5F2=20:=20todo=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=A0=EB=95=8C=20nickname=20=EC=95=88=EB=82=98?= =?UTF-8?q?=EC=98=A4=EB=8A=94=EA=B1=B0=20=EA=B3=A0=EC=B9=A8=20feat=20:=201?= =?UTF-8?q?=5F3=20:=20=EB=82=A0=EC=A7=9C=EB=9E=91=20=EB=82=A0=EC=94=A8?= =?UTF-8?q?=EB=A1=9C=20=EA=B2=80=EC=83=89=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EA=B2=8C=20=EB=A7=8C=EB=93=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../config/AuthUserArgumentResolver.java | 3 +- .../org/example/expert/config/JwtFilter.java | 1 + .../org/example/expert/config/JwtUtil.java | 3 +- .../auth/controller/AuthController.java | 2 ++ .../auth/dto/request/SignupRequest.java | 2 ++ .../domain/auth/service/AuthService.java | 5 ++-- .../comment/service/CommentService.java | 4 +-- .../expert/domain/common/dto/AuthUser.java | 4 ++- .../manager/service/ManagerService.java | 4 +-- .../todo/controller/TodoController.java | 14 ++++++++++ .../todo/repository/TodoRepository.java | 16 +++++++++++ .../domain/todo/service/TodoService.java | 28 +++++++++++++++++-- .../user/controller/UserController.java | 1 + .../user/dto/response/UserResponse.java | 4 ++- .../expert/domain/user/entity/User.java | 13 ++++++--- .../domain/user/service/UserService.java | 2 +- 17 files changed, 90 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index a7fd3e706..d7b4947bb 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + //implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index db00211de..53a8c0991 100644 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java @@ -39,8 +39,9 @@ public Object resolveArgument( // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 Long userId = (Long) request.getAttribute("userId"); String email = (String) request.getAttribute("email"); + String nickname = (String) request.getAttribute("nickname"); UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); - return new AuthUser(userId, email, userRole); + return new AuthUser(userId, email, nickname, userRole); } } diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 03908abe1..1f1ffdd70 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -59,6 +59,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); httpRequest.setAttribute("email", claims.get("email")); + httpRequest.setAttribute("nickname", claims.get("nickname")); httpRequest.setAttribute("userRole", claims.get("userRole")); if (url.startsWith("/admin")) { diff --git a/src/main/java/org/example/expert/config/JwtUtil.java b/src/main/java/org/example/expert/config/JwtUtil.java index 07e0a2c7c..ad4548f05 100644 --- a/src/main/java/org/example/expert/config/JwtUtil.java +++ b/src/main/java/org/example/expert/config/JwtUtil.java @@ -34,13 +34,14 @@ public void init() { key = Keys.hmacShaKeyFor(bytes); } - public String createToken(Long userId, String email, UserRole userRole) { + public String createToken(Long userId, String email, String nickname,UserRole userRole) { Date date = new Date(); return BEARER_PREFIX + Jwts.builder() .setSubject(String.valueOf(userId)) .claim("email", email) + .claim("nickname", nickname) .claim("userRole", userRole) .setExpiration(new Date(date.getTime() + TOKEN_TIME)) .setIssuedAt(date) // 발급일 diff --git a/src/main/java/org/example/expert/domain/auth/controller/AuthController.java b/src/main/java/org/example/expert/domain/auth/controller/AuthController.java index 32d943d0a..709b0fd91 100644 --- a/src/main/java/org/example/expert/domain/auth/controller/AuthController.java +++ b/src/main/java/org/example/expert/domain/auth/controller/AuthController.java @@ -7,6 +7,7 @@ import org.example.expert.domain.auth.dto.response.SigninResponse; import org.example.expert.domain.auth.dto.response.SignupResponse; import org.example.expert.domain.auth.service.AuthService; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -26,4 +27,5 @@ public SignupResponse signup(@Valid @RequestBody SignupRequest signupRequest) { public SigninResponse signin(@Valid @RequestBody SigninRequest signinRequest) { return authService.signin(signinRequest); } + } diff --git a/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java b/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java index cdb103690..1a5eb5b02 100644 --- a/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java +++ b/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java @@ -14,6 +14,8 @@ public class SignupRequest { @NotBlank @Email private String email; @NotBlank + private String nickname; + @NotBlank private String password; @NotBlank private String userRole; diff --git a/src/main/java/org/example/expert/domain/auth/service/AuthService.java b/src/main/java/org/example/expert/domain/auth/service/AuthService.java index a662239dc..fd40a14c7 100644 --- a/src/main/java/org/example/expert/domain/auth/service/AuthService.java +++ b/src/main/java/org/example/expert/domain/auth/service/AuthService.java @@ -37,12 +37,13 @@ public SignupResponse signup(SignupRequest signupRequest) { User newUser = new User( signupRequest.getEmail(), + signupRequest.getNickname(), encodedPassword, userRole ); User savedUser = userRepository.save(newUser); - String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole); + String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), savedUser.getNickname(), userRole); return new SignupResponse(bearerToken); } @@ -56,7 +57,7 @@ public SigninResponse signin(SigninRequest signinRequest) { throw new AuthException("잘못된 비밀번호입니다."); } - String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole()); + String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole()); return new SigninResponse(bearerToken); } diff --git a/src/main/java/org/example/expert/domain/comment/service/CommentService.java b/src/main/java/org/example/expert/domain/comment/service/CommentService.java index 37f857491..3da98ba15 100644 --- a/src/main/java/org/example/expert/domain/comment/service/CommentService.java +++ b/src/main/java/org/example/expert/domain/comment/service/CommentService.java @@ -43,7 +43,7 @@ public CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSa return new CommentSaveResponse( savedComment.getId(), savedComment.getContents(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); } @@ -56,7 +56,7 @@ public List getComments(long todoId) { CommentResponse dto = new CommentResponse( comment.getId(), comment.getContents(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); dtoList.add(dto); } diff --git a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java index 7f4bc52e1..9511d910b 100644 --- a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java +++ b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java @@ -8,11 +8,13 @@ public class AuthUser { private final Long id; private final String email; + private final String nickname; private final UserRole userRole; - public AuthUser(Long id, String email, UserRole userRole) { + public AuthUser(Long id, String email, String nickname,UserRole userRole) { this.id = id; this.email = email; + this.nickname = nickname; this.userRole = userRole; } } diff --git a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java index 9e14df0f1..6f3fdbb3b 100644 --- a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java +++ b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java @@ -52,7 +52,7 @@ public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSa return new ManagerSaveResponse( savedManagerUser.getId(), - new UserResponse(managerUser.getId(), managerUser.getEmail()) + new UserResponse(managerUser.getId(), managerUser.getEmail(), managerUser.getNickname()) ); } @@ -67,7 +67,7 @@ public List getManagers(long todoId) { User user = manager.getUser(); dtoList.add(new ManagerResponse( manager.getId(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) )); } return dtoList; diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index eed1a1b46..9e5c111b7 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -9,9 +9,12 @@ import org.example.expert.domain.todo.dto.response.TodoSaveResponse; import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; + @RestController @RequiredArgsConstructor public class TodoController { @@ -38,4 +41,15 @@ public ResponseEntity> getTodos( public ResponseEntity getTodo(@PathVariable long todoId) { return ResponseEntity.ok(todoService.getTodo(todoId)); } + + @GetMapping("/todo/search") + public Page searchTodo( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String weather, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate + ) { + return todoService.searchTodo(page, size, weather, startDate, endDate); + } } diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java index a3e4e0749..52cddf27c 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; import java.util.Optional; public interface TodoRepository extends JpaRepository { @@ -18,4 +19,19 @@ public interface TodoRepository extends JpaRepository { "LEFT JOIN t.user " + "WHERE t.id = :todoId") Optional findByIdWithUser(@Param("todoId") Long todoId); + + @Query( + "SELECT t FROM Todo t " + + " JOIN FETCH t.user " + + " WHERE (:weather IS NULL OR t.weather = :weather)" + + "AND (: startDate IS NULL OR t.modifiedAt >= :startDate)" + + "AND (: endDate IS NULL OR t.modifiedAt <= :endDate)" + + "ORDER BY t.modifiedAt DESC" + ) + Page findAllByWeatherAndDateRange( + @Param("weather") String weather, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + Pageable pageable + ); } diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 89561fd12..5c20d11fb 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -17,6 +17,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + @Service @RequiredArgsConstructor @Transactional @@ -43,11 +45,12 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ savedTodo.getTitle(), savedTodo.getContents(), weather, - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); } public Page getTodos(int page, int size) { + Pageable pageable = PageRequest.of(page - 1, size); Page todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); @@ -57,7 +60,7 @@ public Page getTodos(int page, int size) { todo.getTitle(), todo.getContents(), todo.getWeather(), - new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), + new UserResponse(todo.getUser().getId(), todo.getUser().getEmail(), todo.getUser().getNickname()), todo.getCreatedAt(), todo.getModifiedAt() )); @@ -74,9 +77,28 @@ public TodoResponse getTodo(long todoId) { todo.getTitle(), todo.getContents(), todo.getWeather(), - new UserResponse(user.getId(), user.getEmail()), + new UserResponse(user.getId(), user.getEmail(), user.getNickname()), todo.getCreatedAt(), todo.getModifiedAt() ); } + + public Page searchTodo(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) { + Pageable pageable = PageRequest.of(page - 1, size); + if (startDate == null) { + startDate = LocalDateTime.of(1, 1, 1,0, 0); + } + if (endDate == null) { + endDate = LocalDateTime.of(9999, 12, 31, 23, 59); + } + Page todos = todoRepository.findAllByWeatherAndDateRange(weather,startDate,endDate, pageable); + + return todos.map(todo -> new TodoResponse( + todo.getId(), todo.getTitle(), todo.getContents(), todo.getWeather(), + new UserResponse(todo.getId(), todo.getUser().getEmail(), todo.getUser().getNickname()), + todo.getCreatedAt(), + todo.getModifiedAt() + )); + } + } diff --git a/src/main/java/org/example/expert/domain/user/controller/UserController.java b/src/main/java/org/example/expert/domain/user/controller/UserController.java index bb1ef7a95..031813287 100644 --- a/src/main/java/org/example/expert/domain/user/controller/UserController.java +++ b/src/main/java/org/example/expert/domain/user/controller/UserController.java @@ -24,4 +24,5 @@ public ResponseEntity getUser(@PathVariable long userId) { public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { userService.changePassword(authUser.getId(), userChangePasswordRequest); } + } diff --git a/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java b/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java index 23794a3ca..91f3240aa 100644 --- a/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java +++ b/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java @@ -7,9 +7,11 @@ public class UserResponse { private final Long id; private final String email; + private final String nickname; - public UserResponse(Long id, String email) { + public UserResponse(Long id, String email, String nickname) { this.id = id; this.email = email; + this.nickname = nickname; } } diff --git a/src/main/java/org/example/expert/domain/user/entity/User.java b/src/main/java/org/example/expert/domain/user/entity/User.java index 30a0cc54f..fd71423cf 100644 --- a/src/main/java/org/example/expert/domain/user/entity/User.java +++ b/src/main/java/org/example/expert/domain/user/entity/User.java @@ -13,28 +13,33 @@ @Table(name = "users") public class User extends Timestamped { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String email; private String password; @Enumerated(EnumType.STRING) private UserRole userRole; + private String nickname; - public User(String email, String password, UserRole userRole) { + public User(String email, String nickname, String password, UserRole userRole) { this.email = email; + this.nickname = nickname; this.password = password; this.userRole = userRole; } - private User(Long id, String email, UserRole userRole) { + private User(Long id, String email, String nickname, UserRole userRole) { this.id = id; this.email = email; + this.nickname = nickname; this.userRole = userRole; } + public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole()); + return new User(authUser.getId(), authUser.getEmail(), authUser.getNickname(), authUser.getUserRole()); } public void changePassword(String password) { diff --git a/src/main/java/org/example/expert/domain/user/service/UserService.java b/src/main/java/org/example/expert/domain/user/service/UserService.java index 15baec417..15bdfa48b 100644 --- a/src/main/java/org/example/expert/domain/user/service/UserService.java +++ b/src/main/java/org/example/expert/domain/user/service/UserService.java @@ -20,7 +20,7 @@ public class UserService { public UserResponse getUser(long userId) { User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found")); - return new UserResponse(user.getId(), user.getEmail()); + return new UserResponse(user.getId(), user.getEmail(), user.getNickname()); } @Transactional From d34b5ef43bb4af74f1bfa25e6d44269ea2f17c89 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Thu, 26 Jun 2025 16:54:46 +0900 Subject: [PATCH 03/15] Lv 1_4 --- .../expert/domain/auth/controller/AuthController.java | 1 - .../domain/todo/controller/TodoControllerTest.java | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/example/expert/domain/auth/controller/AuthController.java b/src/main/java/org/example/expert/domain/auth/controller/AuthController.java index 709b0fd91..c7cf99684 100644 --- a/src/main/java/org/example/expert/domain/auth/controller/AuthController.java +++ b/src/main/java/org/example/expert/domain/auth/controller/AuthController.java @@ -7,7 +7,6 @@ import org.example.expert.domain.auth.dto.response.SigninResponse; import org.example.expert.domain.auth.dto.response.SignupResponse; import org.example.expert.domain.auth.service.AuthService; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; diff --git a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java index 737193874..9d2d04075 100644 --- a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java +++ b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java @@ -35,9 +35,9 @@ class TodoControllerTest { // given long todoId = 1L; String title = "title"; - AuthUser authUser = new AuthUser(1L, "email", UserRole.USER); + AuthUser authUser = new AuthUser(1L, "email", "nickname" ,UserRole.USER); User user = User.fromAuthUser(authUser); - UserResponse userResponse = new UserResponse(user.getId(), user.getEmail()); + UserResponse userResponse = new UserResponse(user.getId(), user.getEmail(), user.getNickname()); TodoResponse response = new TodoResponse( todoId, title, @@ -69,9 +69,9 @@ class TodoControllerTest { // then mockMvc.perform(get("/todos/{todoId}", todoId)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.name())) - .andExpect(jsonPath("$.code").value(HttpStatus.OK.value())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.name())) + .andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST.value())) .andExpect(jsonPath("$.message").value("Todo not found")); } } From b22c565bb0d22b24998fe06a0e792dd004af6ab1 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Thu, 26 Jun 2025 17:53:43 +0900 Subject: [PATCH 04/15] Lv 1_5 --- .../java/org/example/expert/aop/AdminAccessLoggingAspect.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java index c90e8c792..3286b27fd 100644 --- a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java +++ b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java @@ -6,6 +6,7 @@ import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -18,7 +19,7 @@ public class AdminAccessLoggingAspect { private final HttpServletRequest request; - @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") + @Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))") public void logAfterChangeUserRole(JoinPoint joinPoint) { String userId = String.valueOf(request.getAttribute("userId")); String requestUrl = request.getRequestURI(); From a19cd659975c9476acb68eaa91336b88e75af85e Mon Sep 17 00:00:00 2001 From: taehyeon Date: Thu, 26 Jun 2025 19:14:45 +0900 Subject: [PATCH 05/15] Lv 2 : 6 --- src/main/java/org/example/expert/domain/todo/entity/Todo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/todo/entity/Todo.java b/src/main/java/org/example/expert/domain/todo/entity/Todo.java index b4efcced1..6a1bbf3ae 100644 --- a/src/main/java/org/example/expert/domain/todo/entity/Todo.java +++ b/src/main/java/org/example/expert/domain/todo/entity/Todo.java @@ -30,7 +30,7 @@ public class Todo extends Timestamped { @OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE) private List comments = new ArrayList<>(); - @OneToMany(mappedBy = "todo") + @OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST) private List managers = new ArrayList<>(); public Todo(String title, String contents, String weather, User user) { From 0efd2d2c0a575221f8eb1bdc851f17b711c531c9 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Thu, 26 Jun 2025 19:28:10 +0900 Subject: [PATCH 06/15] Lv 2 : 7 --- .../expert/domain/comment/repository/CommentRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java b/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java index 3c97b95dc..4ab2afb7b 100644 --- a/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java +++ b/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java @@ -9,6 +9,7 @@ public interface CommentRepository extends JpaRepository { - @Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId") + // JOIN 이었던거를 JOIN FETCH 로 바꿈 그로 인해서 N + 1 문제를 해결!! + @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId") List findByTodoIdWithUser(@Param("todoId") Long todoId); } From 1f8beea513228219480d0f08abd60bc02225e199 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Fri, 27 Jun 2025 16:42:53 +0900 Subject: [PATCH 07/15] Lv 2 : 8 --- build.gradle | 10 ++- .../domain/comment/entity/QComment.java | 64 +++++++++++++++++ .../domain/common/entity/QTimestamped.java | 39 +++++++++++ .../domain/manager/entity/QManager.java | 54 +++++++++++++++ .../expert/domain/todo/entity/QTodo.java | 69 +++++++++++++++++++ .../expert/domain/user/entity/QUser.java | 53 ++++++++++++++ .../example/expert/config/QueryDslConfig.java | 21 ++++++ .../todo/repository/QTodoRepository.java | 10 +++ .../todo/repository/QTodoRepositoryImpl.java | 28 ++++++++ .../todo/repository/TodoRepository.java | 7 +- .../todo/repository/TodoRepositoryTest.java | 52 ++++++++++++++ 11 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 src/main/generated/org/example/expert/domain/comment/entity/QComment.java create mode 100644 src/main/generated/org/example/expert/domain/common/entity/QTimestamped.java create mode 100644 src/main/generated/org/example/expert/domain/manager/entity/QManager.java create mode 100644 src/main/generated/org/example/expert/domain/todo/entity/QTodo.java create mode 100644 src/main/generated/org/example/expert/domain/user/entity/QUser.java create mode 100644 src/main/java/org/example/expert/config/QueryDslConfig.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java create mode 100644 src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java diff --git a/build.gradle b/build.gradle index d7b4947bb..eaee0418b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.3.3' - id 'io.spring.dependency-management' version '1.1.6' + id 'org.springframework.boot' version '3.4.5' + id 'io.spring.dependency-management' version '1.1.7' } group = 'org.example' @@ -43,6 +43,12 @@ dependencies { compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + + // querydsl 의존성 + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { diff --git a/src/main/generated/org/example/expert/domain/comment/entity/QComment.java b/src/main/generated/org/example/expert/domain/comment/entity/QComment.java new file mode 100644 index 000000000..a2aec389f --- /dev/null +++ b/src/main/generated/org/example/expert/domain/comment/entity/QComment.java @@ -0,0 +1,64 @@ +package org.example.expert.domain.comment.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QComment is a Querydsl query type for Comment + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QComment extends EntityPathBase { + + private static final long serialVersionUID = 1329458967L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QComment comment = new QComment("comment"); + + public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this); + + public final StringPath contents = createString("contents"); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath modifiedAt = _super.modifiedAt; + + public final org.example.expert.domain.todo.entity.QTodo todo; + + public final org.example.expert.domain.user.entity.QUser user; + + public QComment(String variable) { + this(Comment.class, forVariable(variable), INITS); + } + + public QComment(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QComment(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QComment(PathMetadata metadata, PathInits inits) { + this(Comment.class, metadata, inits); + } + + public QComment(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.todo = inits.isInitialized("todo") ? new org.example.expert.domain.todo.entity.QTodo(forProperty("todo"), inits.get("todo")) : null; + this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/org/example/expert/domain/common/entity/QTimestamped.java b/src/main/generated/org/example/expert/domain/common/entity/QTimestamped.java new file mode 100644 index 000000000..cc062d17a --- /dev/null +++ b/src/main/generated/org/example/expert/domain/common/entity/QTimestamped.java @@ -0,0 +1,39 @@ +package org.example.expert.domain.common.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QTimestamped is a Querydsl query type for Timestamped + */ +@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") +public class QTimestamped extends EntityPathBase { + + private static final long serialVersionUID = -1617243527L; + + public static final QTimestamped timestamped = new QTimestamped("timestamped"); + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); + + public final DateTimePath modifiedAt = createDateTime("modifiedAt", java.time.LocalDateTime.class); + + public QTimestamped(String variable) { + super(Timestamped.class, forVariable(variable)); + } + + public QTimestamped(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QTimestamped(PathMetadata metadata) { + super(Timestamped.class, metadata); + } + +} + diff --git a/src/main/generated/org/example/expert/domain/manager/entity/QManager.java b/src/main/generated/org/example/expert/domain/manager/entity/QManager.java new file mode 100644 index 000000000..cd3eb8edb --- /dev/null +++ b/src/main/generated/org/example/expert/domain/manager/entity/QManager.java @@ -0,0 +1,54 @@ +package org.example.expert.domain.manager.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QManager is a Querydsl query type for Manager + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QManager extends EntityPathBase { + + private static final long serialVersionUID = 216623447L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QManager manager = new QManager("manager"); + + public final NumberPath id = createNumber("id", Long.class); + + public final org.example.expert.domain.todo.entity.QTodo todo; + + public final org.example.expert.domain.user.entity.QUser user; + + public QManager(String variable) { + this(Manager.class, forVariable(variable), INITS); + } + + public QManager(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QManager(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QManager(PathMetadata metadata, PathInits inits) { + this(Manager.class, metadata, inits); + } + + public QManager(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.todo = inits.isInitialized("todo") ? new org.example.expert.domain.todo.entity.QTodo(forProperty("todo"), inits.get("todo")) : null; + this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/org/example/expert/domain/todo/entity/QTodo.java b/src/main/generated/org/example/expert/domain/todo/entity/QTodo.java new file mode 100644 index 000000000..e6bf31f74 --- /dev/null +++ b/src/main/generated/org/example/expert/domain/todo/entity/QTodo.java @@ -0,0 +1,69 @@ +package org.example.expert.domain.todo.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QTodo is a Querydsl query type for Todo + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QTodo extends EntityPathBase { + + private static final long serialVersionUID = -1664369315L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QTodo todo = new QTodo("todo"); + + public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this); + + public final ListPath comments = this.createList("comments", org.example.expert.domain.comment.entity.Comment.class, org.example.expert.domain.comment.entity.QComment.class, PathInits.DIRECT2); + + public final StringPath contents = createString("contents"); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final NumberPath id = createNumber("id", Long.class); + + public final ListPath managers = this.createList("managers", org.example.expert.domain.manager.entity.Manager.class, org.example.expert.domain.manager.entity.QManager.class, PathInits.DIRECT2); + + //inherited + public final DateTimePath modifiedAt = _super.modifiedAt; + + public final StringPath title = createString("title"); + + public final org.example.expert.domain.user.entity.QUser user; + + public final StringPath weather = createString("weather"); + + public QTodo(String variable) { + this(Todo.class, forVariable(variable), INITS); + } + + public QTodo(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QTodo(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QTodo(PathMetadata metadata, PathInits inits) { + this(Todo.class, metadata, inits); + } + + public QTodo(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/org/example/expert/domain/user/entity/QUser.java b/src/main/generated/org/example/expert/domain/user/entity/QUser.java new file mode 100644 index 000000000..1faeeb9da --- /dev/null +++ b/src/main/generated/org/example/expert/domain/user/entity/QUser.java @@ -0,0 +1,53 @@ +package org.example.expert.domain.user.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QUser is a Querydsl query type for User + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QUser extends EntityPathBase { + + private static final long serialVersionUID = -1825397529L; + + public static final QUser user = new QUser("user"); + + public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this); + + //inherited + public final DateTimePath createdAt = _super.createdAt; + + public final StringPath email = createString("email"); + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath modifiedAt = _super.modifiedAt; + + public final StringPath nickname = createString("nickname"); + + public final StringPath password = createString("password"); + + public final EnumPath userRole = createEnum("userRole", org.example.expert.domain.user.enums.UserRole.class); + + public QUser(String variable) { + super(User.class, forVariable(variable)); + } + + public QUser(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QUser(PathMetadata metadata) { + super(User.class, metadata); + } + +} + diff --git a/src/main/java/org/example/expert/config/QueryDslConfig.java b/src/main/java/org/example/expert/config/QueryDslConfig.java new file mode 100644 index 000000000..0a21b3eac --- /dev/null +++ b/src/main/java/org/example/expert/config/QueryDslConfig.java @@ -0,0 +1,21 @@ +package org.example.expert.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +// querydsl 사용을 위한 사전준비 +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java new file mode 100644 index 000000000..9e81c2293 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java @@ -0,0 +1,10 @@ +package org.example.expert.domain.todo.repository; + +import org.example.expert.domain.todo.entity.Todo; + +import java.util.Optional; + +public interface QTodoRepository { + + Optional findByIdWithUser(long id); +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java new file mode 100644 index 000000000..d2046ba1c --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java @@ -0,0 +1,28 @@ +package org.example.expert.domain.todo.repository; + + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.todo.entity.Todo; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static org.example.expert.domain.todo.entity.QTodo.todo; +import static org.example.expert.domain.user.entity.QUser.user; + +@Repository +@RequiredArgsConstructor +public class QTodoRepositoryImpl implements QTodoRepository{ + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findByIdWithUser(long id) { + return Optional.ofNullable( + queryFactory.selectFrom(todo) + .leftJoin(todo.user, user).fetchJoin() + .where(todo.id.eq(id)) + .fetchOne()); + } +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java index 52cddf27c..667664637 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java @@ -10,15 +10,12 @@ import java.time.LocalDateTime; import java.util.Optional; -public interface TodoRepository extends JpaRepository { +public interface TodoRepository extends JpaRepository, QTodoRepository { @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") Page findAllByOrderByModifiedAtDesc(Pageable pageable); - @Query("SELECT t FROM Todo t " + - "LEFT JOIN t.user " + - "WHERE t.id = :todoId") - Optional findByIdWithUser(@Param("todoId") Long todoId); + @Query( "SELECT t FROM Todo t " + diff --git a/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java b/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java new file mode 100644 index 000000000..df690a0ce --- /dev/null +++ b/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java @@ -0,0 +1,52 @@ +package org.example.expert.domain.todo.repository; + +import org.example.expert.config.QueryDslConfig; +import org.example.expert.domain.todo.entity.Todo; +import org.example.expert.domain.user.entity.User; +import org.example.expert.domain.user.enums.UserRole; +import org.example.expert.domain.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DataJpaTest +@DisplayName("Repository:Todo") +@ActiveProfiles("test") +@Import(QueryDslConfig.class) +class TodoRepositoryTest { + @Autowired + private UserRepository userRepository; + + @Autowired + private TodoRepository todoRepository; + + private User user; + + @BeforeEach + void setUp() { + user = new User("hong-gd@gmail.com", "nickname","password", UserRole.USER); + userRepository.save(user); + + Todo todo = new Todo("todo title", "todo contents", "sunny", user); + todoRepository.save(todo); + } + + @Test + @DisplayName("QueryDsl로 전환한 findByIdWithUser의 동작 검증") + void findByIdWithUser() { + // Given + Long userId = user.getId(); + + // When + Todo todo = todoRepository.findByIdWithUser(userId).orElseThrow(); + + // Then + assertThat(todo).isNotNull(); + } +} \ No newline at end of file From 31e06ed5318ee5edb0bace6e61c92449afc12d6a Mon Sep 17 00:00:00 2001 From: taehyeon Date: Mon, 30 Jun 2025 14:50:44 +0900 Subject: [PATCH 08/15] Lv 2 : 9 --- build.gradle | 4 +- .../config/AuthUserArgumentResolver.java | 68 +++++++++---------- .../example/expert/config/FilterConfig.java | 32 ++++----- .../org/example/expert/config/WebConfig.java | 20 +++--- .../security/CustomAccessDeniedHandler.java | 39 +++++++++++ .../CustomAuthenticationEntryPoint.java | 38 +++++++++++ .../expert/config/security/ErrorResponse.java | 13 ++++ .../config/security/SecurityConfig.java | 62 +++++++++++++++++ .../comment/controller/CommentController.java | 4 +- .../manager/controller/ManagerController.java | 5 +- .../todo/controller/TodoController.java | 5 +- .../todo/repository/QTodoRepository.java | 2 +- .../todo/repository/QTodoRepositoryImpl.java | 2 +- .../user/controller/UserAdminController.java | 4 +- .../user/controller/UserController.java | 5 +- .../todo/controller/TodoControllerTest.java | 4 +- .../todo/repository/TodoRepositoryTest.java | 4 +- 17 files changed, 236 insertions(+), 75 deletions(-) create mode 100644 src/main/java/org/example/expert/config/security/CustomAccessDeniedHandler.java create mode 100644 src/main/java/org/example/expert/config/security/CustomAuthenticationEntryPoint.java create mode 100644 src/main/java/org/example/expert/config/security/ErrorResponse.java create mode 100644 src/main/java/org/example/expert/config/security/SecurityConfig.java diff --git a/build.gradle b/build.gradle index eaee0418b..742188be5 100644 --- a/build.gradle +++ b/build.gradle @@ -27,10 +27,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' + testRuntimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index 53a8c0991..26ef47aa8 100644 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java @@ -11,37 +11,37 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; - -public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { - - @Override - public boolean supportsParameter(MethodParameter parameter) { - boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null; - boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class); - - // @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생 - if (hasAuthAnnotation != isAuthUserType) { - throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다."); - } - - return hasAuthAnnotation; - } - - @Override - public Object resolveArgument( - @Nullable MethodParameter parameter, - @Nullable ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - @Nullable WebDataBinderFactory binderFactory - ) { - HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - - // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 - Long userId = (Long) request.getAttribute("userId"); - String email = (String) request.getAttribute("email"); - String nickname = (String) request.getAttribute("nickname"); - UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); - - return new AuthUser(userId, email, nickname, userRole); - } -} +// +//public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { +// +// @Override +// public boolean supportsParameter(MethodParameter parameter) { +// boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null; +// boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class); +// +// // @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생 +// if (hasAuthAnnotation != isAuthUserType) { +// throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다."); +// } +// +// return hasAuthAnnotation; +// } +// +// @Override +// public Object resolveArgument( +// @Nullable MethodParameter parameter, +// @Nullable ModelAndViewContainer mavContainer, +// NativeWebRequest webRequest, +// @Nullable WebDataBinderFactory binderFactory +// ) { +// HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); +// +// // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 +// Long userId = (Long) request.getAttribute("userId"); +// String email = (String) request.getAttribute("email"); +// String nickname = (String) request.getAttribute("nickname"); +// UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); +// +// return new AuthUser(userId, email, nickname, userRole); +// } +//} diff --git a/src/main/java/org/example/expert/config/FilterConfig.java b/src/main/java/org/example/expert/config/FilterConfig.java index 34cb4088a..f86b74fa7 100644 --- a/src/main/java/org/example/expert/config/FilterConfig.java +++ b/src/main/java/org/example/expert/config/FilterConfig.java @@ -4,19 +4,19 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -@Configuration -@RequiredArgsConstructor -public class FilterConfig { - - private final JwtUtil jwtUtil; - - @Bean - public FilterRegistrationBean jwtFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new JwtFilter(jwtUtil)); - registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다. - - return registrationBean; - } -} +// +//@Configuration +//@RequiredArgsConstructor +//public class FilterConfig { +// +// private final JwtUtil jwtUtil; +// +// @Bean +// public FilterRegistrationBean jwtFilter() { +// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); +// registrationBean.setFilter(new JwtFilter(jwtUtil)); +// registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다. +// +// return registrationBean; +// } +//} diff --git a/src/main/java/org/example/expert/config/WebConfig.java b/src/main/java/org/example/expert/config/WebConfig.java index adff06b82..5690611ea 100644 --- a/src/main/java/org/example/expert/config/WebConfig.java +++ b/src/main/java/org/example/expert/config/WebConfig.java @@ -7,13 +7,13 @@ import java.util.List; -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - // ArgumentResolver 등록 - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(new AuthUserArgumentResolver()); - } -} +//@Configuration +//@RequiredArgsConstructor +//public class WebConfig implements WebMvcConfigurer { +// +// // ArgumentResolver 등록 +// @Override +// public void addArgumentResolvers(List resolvers) { +// resolvers.add(new AuthUserArgumentResolver()); +// } +//} diff --git a/src/main/java/org/example/expert/config/security/CustomAccessDeniedHandler.java b/src/main/java/org/example/expert/config/security/CustomAccessDeniedHandler.java new file mode 100644 index 000000000..414116cbf --- /dev/null +++ b/src/main/java/org/example/expert/config/security/CustomAccessDeniedHandler.java @@ -0,0 +1,39 @@ +package org.example.expert.config.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + + log.error("No Authorities", accessDeniedException); + log.error("Request Uri : {}", request.getRequestURI()); + + ErrorResponse errorResponse = new ErrorResponse(HttpStatus.FORBIDDEN.value(), accessDeniedException.getMessage()); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(responseBody); + } +} diff --git a/src/main/java/org/example/expert/config/security/CustomAuthenticationEntryPoint.java b/src/main/java/org/example/expert/config/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 000000000..307b5c99c --- /dev/null +++ b/src/main/java/org/example/expert/config/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,38 @@ +package org.example.expert.config.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + // 어떻게 처리할지 여기서 커스텀해서 시큐리티컨피규어에 등록을 해줘야함 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + + log.error("Not Authenticated Request", authException); + log.error("Request Uri : {}", request.getRequestURI()); + + ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED.value(), authException.getMessage()); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(responseBody); + } +} diff --git a/src/main/java/org/example/expert/config/security/ErrorResponse.java b/src/main/java/org/example/expert/config/security/ErrorResponse.java new file mode 100644 index 000000000..15b67da35 --- /dev/null +++ b/src/main/java/org/example/expert/config/security/ErrorResponse.java @@ -0,0 +1,13 @@ +package org.example.expert.config.security; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + + private final int status; + private final String message; + +} diff --git a/src/main/java/org/example/expert/config/security/SecurityConfig.java b/src/main/java/org/example/expert/config/security/SecurityConfig.java new file mode 100644 index 000000000..5b276af19 --- /dev/null +++ b/src/main/java/org/example/expert/config/security/SecurityConfig.java @@ -0,0 +1,62 @@ +package org.example.expert.config.security; + + +import lombok.RequiredArgsConstructor; +import org.example.expert.config.JwtFilter; +import org.example.expert.config.JwtUtil; +import org.example.expert.domain.user.enums.UserRole; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + + return httpSecurity + .cors(Customizer.withDefaults()) //Cross-Origin Resource Sharing // 프론트엔드랑 협업할때 주로 사용 + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + + .sessionManagement(session -> session // session 안쓰겠다고 알리는거 + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // config로 url에 대한 인증/인가를 관리 + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/signup").permitAll() + .requestMatchers("/auth/signin").permitAll() + .requestMatchers(HttpMethod.POST, "/todos").hasAuthority(UserRole.USER.name()) + .requestMatchers(HttpMethod.GET, "/todos").hasRole(UserRole.USER.name()) + .requestMatchers(HttpMethod.GET, "/todos/**").hasRole(UserRole.USER.name()) + .requestMatchers(HttpMethod.PATCH, "/admin/users/").hasRole(UserRole.ADMIN.name()) + .anyRequest().denyAll() + ) + + //필터 등록 + .addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class) + // 에러 처리까지 완벽하게 하고 싶다면 exceptionHandling 을 등록을 해야한다. + .exceptionHandling(configurer -> + configurer + .authenticationEntryPoint(customAuthenticationEntryPoint) // 인증할때 발생하는 오류를 처리하는... + .accessDeniedHandler(customAccessDeniedHandler) // 인가처리할때 발생하는 오류를 처리하는 ... + ) + .build(); + } +} diff --git a/src/main/java/org/example/expert/domain/comment/controller/CommentController.java b/src/main/java/org/example/expert/domain/comment/controller/CommentController.java index 51264b12e..e8734a2ef 100644 --- a/src/main/java/org/example/expert/domain/comment/controller/CommentController.java +++ b/src/main/java/org/example/expert/domain/comment/controller/CommentController.java @@ -6,9 +6,9 @@ import org.example.expert.domain.comment.dto.response.CommentResponse; import org.example.expert.domain.comment.dto.response.CommentSaveResponse; import org.example.expert.domain.comment.service.CommentService; -import org.example.expert.domain.common.annotation.Auth; import org.example.expert.domain.common.dto.AuthUser; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,7 +21,7 @@ public class CommentController { @PostMapping("/todos/{todoId}/comments") public ResponseEntity saveComment( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthUser authUser, @PathVariable long todoId, @Valid @RequestBody CommentSaveRequest commentSaveRequest ) { diff --git a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java index 327b6452b..66f3a4971 100644 --- a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java +++ b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java @@ -9,6 +9,7 @@ import org.example.expert.domain.manager.dto.response.ManagerSaveResponse; import org.example.expert.domain.manager.service.ManagerService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,7 +22,7 @@ public class ManagerController { @PostMapping("/todos/{todoId}/managers") public ResponseEntity saveManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthUser authUser, @PathVariable long todoId, @Valid @RequestBody ManagerSaveRequest managerSaveRequest ) { @@ -35,7 +36,7 @@ public ResponseEntity> getMembers(@PathVariable long todoI @DeleteMapping("/todos/{todoId}/managers/{managerId}") public void deleteManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthUser authUser, @PathVariable long todoId, @PathVariable long managerId ) { diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index 9e5c111b7..078f4f766 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -11,6 +11,7 @@ import org.springframework.data.domain.Page; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @@ -23,7 +24,7 @@ public class TodoController { @PostMapping("/todos") public ResponseEntity saveTodo( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthUser authUser, @Valid @RequestBody TodoSaveRequest todoSaveRequest ) { return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest)); @@ -42,7 +43,7 @@ public ResponseEntity getTodo(@PathVariable long todoId) { return ResponseEntity.ok(todoService.getTodo(todoId)); } - @GetMapping("/todo/search") + @GetMapping("/todos/search") public Page searchTodo( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java index 9e81c2293..efe4ead28 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java @@ -6,5 +6,5 @@ public interface QTodoRepository { - Optional findByIdWithUser(long id); + Optional findByIdWithUser(Long id); } diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java index d2046ba1c..9f34569f8 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java @@ -18,7 +18,7 @@ public class QTodoRepositoryImpl implements QTodoRepository{ private final JPAQueryFactory queryFactory; @Override - public Optional findByIdWithUser(long id) { + public Optional findByIdWithUser(Long id) { return Optional.ofNullable( queryFactory.selectFrom(todo) .leftJoin(todo.user, user).fetchJoin() diff --git a/src/main/java/org/example/expert/domain/user/controller/UserAdminController.java b/src/main/java/org/example/expert/domain/user/controller/UserAdminController.java index 53d45c8b5..7be019c62 100644 --- a/src/main/java/org/example/expert/domain/user/controller/UserAdminController.java +++ b/src/main/java/org/example/expert/domain/user/controller/UserAdminController.java @@ -15,7 +15,9 @@ public class UserAdminController { private final UserAdminService userAdminService; @PatchMapping("/admin/users/{userId}") - public void changeUserRole(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest) { + public void changeUserRole( + @PathVariable long userId, + @RequestBody UserRoleChangeRequest userRoleChangeRequest) { userAdminService.changeUserRole(userId, userRoleChangeRequest); } } diff --git a/src/main/java/org/example/expert/domain/user/controller/UserController.java b/src/main/java/org/example/expert/domain/user/controller/UserController.java index 031813287..d5403efe1 100644 --- a/src/main/java/org/example/expert/domain/user/controller/UserController.java +++ b/src/main/java/org/example/expert/domain/user/controller/UserController.java @@ -7,6 +7,7 @@ import org.example.expert.domain.user.dto.response.UserResponse; import org.example.expert.domain.user.service.UserService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -21,7 +22,9 @@ public ResponseEntity getUser(@PathVariable long userId) { } @PutMapping("/users") - public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { + public void changePassword( + @AuthenticationPrincipal AuthUser authUser, + @RequestBody UserChangePasswordRequest userChangePasswordRequest) { userService.changePassword(authUser.getId(), userChangePasswordRequest); } diff --git a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java index 9d2d04075..27d1e216e 100644 --- a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java +++ b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java @@ -8,9 +8,9 @@ import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.enums.UserRole; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import org.springframework.test.web.servlet.MockMvc; @@ -27,7 +27,7 @@ class TodoControllerTest { @Autowired private MockMvc mockMvc; - @MockBean + @Mock private TodoService todoService; @Test diff --git a/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java b/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java index df690a0ce..c0c3e24b8 100644 --- a/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java +++ b/src/test/java/org/example/expert/domain/todo/repository/TodoRepositoryTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; @@ -17,7 +18,8 @@ @DataJpaTest @DisplayName("Repository:Todo") -@ActiveProfiles("test") +//@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import(QueryDslConfig.class) class TodoRepositoryTest { @Autowired From c59aa09d36ef6ce3ad74655a03a3245caf72a850 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Mon, 30 Jun 2025 15:27:02 +0900 Subject: [PATCH 09/15] Lv 2 : 9 --- .../org/example/expert/config/JwtFilter.java | 25 +++++++++++++++---- .../org/example/expert/config/JwtUtil.java | 18 ++++++++++++- .../config/security/SecurityConfig.java | 2 +- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 1f1ffdd70..26cf8ef21 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -10,9 +10,14 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import java.io.IOException; +import java.util.List; @Slf4j @RequiredArgsConstructor @@ -55,12 +60,22 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha return; } - UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class)); + Long userId = jwtUtil.getUserId(jwt); + String email = jwtUtil.getEmail(jwt); + String nickname = jwtUtil.getNickName(jwt); + UserRole userRole = jwtUtil.getUserRole(jwt); - httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); - httpRequest.setAttribute("email", claims.get("email")); - httpRequest.setAttribute("nickname", claims.get("nickname")); - httpRequest.setAttribute("userRole", claims.get("userRole")); + AuthUser authUser = new AuthUser(userId, email, nickname, userRole); + + // 인증 객체를 직접만들어준다... + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + authUser, "", List.of(new SimpleGrantedAuthority("ROLE_" + userRole.name())) // 권한까지 부여 + ); + + + + // 거를 contextholder에 대입 + SecurityContextHolder.getContext().setAuthentication(authenticationToken); if (url.startsWith("/admin")) { // 관리자 권한이 없는 경우 403을 반환합니다. diff --git a/src/main/java/org/example/expert/config/JwtUtil.java b/src/main/java/org/example/expert/config/JwtUtil.java index ad4548f05..a8370d664 100644 --- a/src/main/java/org/example/expert/config/JwtUtil.java +++ b/src/main/java/org/example/expert/config/JwtUtil.java @@ -34,7 +34,7 @@ public void init() { key = Keys.hmacShaKeyFor(bytes); } - public String createToken(Long userId, String email, String nickname,UserRole userRole) { + public String createToken(Long userId, String email, String nickname, UserRole userRole) { Date date = new Date(); return BEARER_PREFIX + @@ -63,4 +63,20 @@ public Claims extractClaims(String token) { .parseClaimsJws(token) .getBody(); } + + public Long getUserId(String token) { + return Long.parseLong(extractClaims(token).getSubject()); + } + + public String getEmail(String token) { + return extractClaims(token).get("email", String.class); + } + + public String getNickName(String token) { + return extractClaims(token).get("nickname", String.class); + } + + public UserRole getUserRole(String token) { + return UserRole.of(extractClaims(token).get("userRole", String.class)); + } } diff --git a/src/main/java/org/example/expert/config/security/SecurityConfig.java b/src/main/java/org/example/expert/config/security/SecurityConfig.java index 5b276af19..020846640 100644 --- a/src/main/java/org/example/expert/config/security/SecurityConfig.java +++ b/src/main/java/org/example/expert/config/security/SecurityConfig.java @@ -42,7 +42,7 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/signup").permitAll() .requestMatchers("/auth/signin").permitAll() - .requestMatchers(HttpMethod.POST, "/todos").hasAuthority(UserRole.USER.name()) + .requestMatchers(HttpMethod.POST, "/todos").hasRole(UserRole.USER.name()) .requestMatchers(HttpMethod.GET, "/todos").hasRole(UserRole.USER.name()) .requestMatchers(HttpMethod.GET, "/todos/**").hasRole(UserRole.USER.name()) .requestMatchers(HttpMethod.PATCH, "/admin/users/").hasRole(UserRole.ADMIN.name()) From 8cc879e220b4ec59e89ca55e00d4b68b1f0cc6f1 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Tue, 1 Jul 2025 15:03:11 +0900 Subject: [PATCH 10/15] Lv 3 : 10 --- .../config/security/SecurityConfig.java | 6 +- .../manager/controller/ManagerController.java | 10 ++ .../dto/request/ManagerSaveRequest.java | 1 + .../manager/repository/ManagerRepository.java | 2 +- .../repository/QManagerRepository.java | 10 ++ .../repository/QManagerRepositoryImpl.java | 54 +++++++ .../manager/service/ManagerService.java | 16 +++ .../todo/controller/TodoController.java | 23 ++- .../todo/dto/response/TodoResponse.java | 3 + .../dto/response/TodoSummaryResponseDto.java | 21 +++ .../todo/repository/QTodoRepository.java | 8 ++ .../todo/repository/QTodoRepositoryImpl.java | 134 +++++++++++++++++- .../todo/repository/TodoRepository.java | 19 --- .../domain/todo/service/TodoService.java | 30 +++- 14 files changed, 301 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/example/expert/domain/manager/repository/QManagerRepository.java create mode 100644 src/main/java/org/example/expert/domain/manager/repository/QManagerRepositoryImpl.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java diff --git a/src/main/java/org/example/expert/config/security/SecurityConfig.java b/src/main/java/org/example/expert/config/security/SecurityConfig.java index 020846640..4ca785961 100644 --- a/src/main/java/org/example/expert/config/security/SecurityConfig.java +++ b/src/main/java/org/example/expert/config/security/SecurityConfig.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.example.expert.config.JwtFilter; import org.example.expert.config.JwtUtil; +import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.enums.UserRole; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,9 +43,8 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/signup").permitAll() .requestMatchers("/auth/signin").permitAll() - .requestMatchers(HttpMethod.POST, "/todos").hasRole(UserRole.USER.name()) - .requestMatchers(HttpMethod.GET, "/todos").hasRole(UserRole.USER.name()) - .requestMatchers(HttpMethod.GET, "/todos/**").hasRole(UserRole.USER.name()) + .requestMatchers("/todos").hasAnyRole(UserRole.USER.name(), UserRole.ADMIN.name()) // 유저랑 어드민 둘다 허용 + .requestMatchers("/todos/**").hasAnyRole(UserRole.USER.name(), UserRole.ADMIN.name()) .requestMatchers(HttpMethod.PATCH, "/admin/users/").hasRole(UserRole.ADMIN.name()) .anyRequest().denyAll() ) diff --git a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java index 66f3a4971..b2c6a9448 100644 --- a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java +++ b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java @@ -8,6 +8,8 @@ import org.example.expert.domain.manager.dto.response.ManagerResponse; import org.example.expert.domain.manager.dto.response.ManagerSaveResponse; import org.example.expert.domain.manager.service.ManagerService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -42,4 +44,12 @@ public void deleteManager( ) { managerService.deleteManager(authUser, todoId, managerId); } + + @GetMapping("/todos/managers") + public Page findManager( + @RequestParam String nickname, + Pageable pageable + ) { + return managerService.findManger(nickname, pageable); + } } diff --git a/src/main/java/org/example/expert/domain/manager/dto/request/ManagerSaveRequest.java b/src/main/java/org/example/expert/domain/manager/dto/request/ManagerSaveRequest.java index 9af5f3a9c..abedd64fd 100644 --- a/src/main/java/org/example/expert/domain/manager/dto/request/ManagerSaveRequest.java +++ b/src/main/java/org/example/expert/domain/manager/dto/request/ManagerSaveRequest.java @@ -12,4 +12,5 @@ public class ManagerSaveRequest { @NotNull private Long managerUserId; // 일정 작상자가 배치하는 유저 id + } diff --git a/src/main/java/org/example/expert/domain/manager/repository/ManagerRepository.java b/src/main/java/org/example/expert/domain/manager/repository/ManagerRepository.java index 23acf898b..bda5bac69 100644 --- a/src/main/java/org/example/expert/domain/manager/repository/ManagerRepository.java +++ b/src/main/java/org/example/expert/domain/manager/repository/ManagerRepository.java @@ -7,7 +7,7 @@ import java.util.List; -public interface ManagerRepository extends JpaRepository { +public interface ManagerRepository extends JpaRepository, QManagerRepository { @Query("SELECT m FROM Manager m JOIN FETCH m.user WHERE m.todo.id = :todoId") List findByTodoIdWithUser(@Param("todoId") Long todoId); } diff --git a/src/main/java/org/example/expert/domain/manager/repository/QManagerRepository.java b/src/main/java/org/example/expert/domain/manager/repository/QManagerRepository.java new file mode 100644 index 000000000..eb93e5bcb --- /dev/null +++ b/src/main/java/org/example/expert/domain/manager/repository/QManagerRepository.java @@ -0,0 +1,10 @@ +package org.example.expert.domain.manager.repository; + +import org.example.expert.domain.manager.dto.response.ManagerResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QManagerRepository { + + Page findManager(String nickname, Pageable pageable); +} diff --git a/src/main/java/org/example/expert/domain/manager/repository/QManagerRepositoryImpl.java b/src/main/java/org/example/expert/domain/manager/repository/QManagerRepositoryImpl.java new file mode 100644 index 000000000..20611235a --- /dev/null +++ b/src/main/java/org/example/expert/domain/manager/repository/QManagerRepositoryImpl.java @@ -0,0 +1,54 @@ +package org.example.expert.domain.manager.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.manager.dto.response.ManagerResponse; +import org.example.expert.domain.user.dto.response.UserResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static org.example.expert.domain.manager.entity.QManager.manager; +import static org.example.expert.domain.user.entity.QUser.user; + +@Repository +@RequiredArgsConstructor +public class QManagerRepositoryImpl implements QManagerRepository{ + + private final JPAQueryFactory queryFactory; + + @Override + public Page findManager(String nickname, Pageable pageable) { + List content = queryFactory + .select(Projections.constructor( + ManagerResponse.class, + manager.id, + Projections.constructor( + UserResponse.class, + manager.user.id, + manager.user.email, + manager.user.nickname + ) + )) + .from(manager) + .join(manager.user, user) + .where(user.nickname.containsIgnoreCase(nickname)) + .orderBy(manager.todo.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(manager.count()) + .from(manager) + .join(manager.user, user) + .where(user.nickname.containsIgnoreCase(nickname)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0); + } +} diff --git a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java index 6f3fdbb3b..9cee6954c 100644 --- a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java +++ b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java @@ -13,6 +13,8 @@ import org.example.expert.domain.user.dto.response.UserResponse; import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; @@ -93,4 +95,18 @@ public void deleteManager(AuthUser authUser, long todoId, long managerId) { managerRepository.delete(manager); } + + public Page findManger(String nickname, Pageable pageable) { + Page manager = managerRepository.findManager(nickname, pageable); + return manager.map( + managerResponse -> new ManagerResponse( + managerResponse.getId(), + new UserResponse( + managerResponse.getId(), + managerResponse.getUser().getEmail(), + managerResponse.getUser().getNickname() + ) + ) + ); + } } diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index 078f4f766..b0df65406 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -2,19 +2,20 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.dto.response.TodoSaveResponse; +import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; +import java.time.LocalDate; @RestController @RequiredArgsConstructor @@ -48,9 +49,23 @@ public Page searchTodo( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String weather, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDate endDate ) { return todoService.searchTodo(page, size, weather, startDate, endDate); } + + @GetMapping("/todos/title") + public Page searchTitle( + @RequestParam String keyword, + Pageable pageable + ) { + return todoService.searchTitle(keyword, pageable); + } + + + @GetMapping("/todos/summary") + public Page findTodoSummary(Pageable pageable) { + return todoService.findTodoSummary(pageable); + } } diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java index 3ec935e4b..d72345380 100644 --- a/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java @@ -1,6 +1,9 @@ package org.example.expert.domain.todo.dto.response; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import org.example.expert.domain.user.dto.response.UserResponse; import java.time.LocalDateTime; diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java new file mode 100644 index 000000000..939b4fb8c --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java @@ -0,0 +1,21 @@ +package org.example.expert.domain.todo.dto.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TodoSummaryResponseDto { + + private Long todoId; + private String title; + private Long managerCount; + private Long commentCount; + + public TodoSummaryResponseDto(Long todoId, String title, Long managerCount, Long commentCount) { + this.todoId = todoId; + this.title = title; + this.managerCount = managerCount; + this.commentCount = commentCount; + } +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java index efe4ead28..311fbdec3 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java @@ -1,10 +1,18 @@ package org.example.expert.domain.todo.repository; +import org.example.expert.domain.todo.dto.response.TodoResponse; +import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; import org.example.expert.domain.todo.entity.Todo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import java.time.LocalDate; import java.util.Optional; public interface QTodoRepository { Optional findByIdWithUser(Long id); + Page findByTitle(String keyword, Pageable pageable); + Page findAllByWeatherAndDateRange(String weather, LocalDate startDate, LocalDate endDate, Pageable pageable); + Page findTodoSummary(Pageable pageable); } diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java index 9f34569f8..f07ceaf70 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java @@ -1,19 +1,32 @@ package org.example.expert.domain.todo.repository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.example.expert.domain.todo.dto.response.TodoResponse; +import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; import org.example.expert.domain.todo.entity.Todo; +import org.example.expert.domain.user.dto.response.UserResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; +import static org.example.expert.domain.comment.entity.QComment.comment; +import static org.example.expert.domain.manager.entity.QManager.manager; import static org.example.expert.domain.todo.entity.QTodo.todo; import static org.example.expert.domain.user.entity.QUser.user; @Repository @RequiredArgsConstructor -public class QTodoRepositoryImpl implements QTodoRepository{ +public class QTodoRepositoryImpl implements QTodoRepository { private final JPAQueryFactory queryFactory; @@ -21,8 +34,121 @@ public class QTodoRepositoryImpl implements QTodoRepository{ public Optional findByIdWithUser(Long id) { return Optional.ofNullable( queryFactory.selectFrom(todo) - .leftJoin(todo.user, user).fetchJoin() - .where(todo.id.eq(id)) - .fetchOne()); + .leftJoin(todo.user, user).fetchJoin() + .where(todo.id.eq(id)) + .fetchOne()); } + + @Override + public Page findByTitle(String keyword, Pageable pageable) { + // 1. 엔티티로 조회 + List content = queryFactory + .selectFrom(todo) + .where(todo.title.containsIgnoreCase(keyword)) + .orderBy(todo.modifiedAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(todo.count()) + .from(todo) + .where(todo.title.containsIgnoreCase(keyword)) + .fetchOne(); + + List dtoList = content.stream() + .map(t -> new TodoResponse( + t.getId(), + t.getTitle(), + t.getContents(), + t.getWeather(), + new UserResponse( + t.getUser().getId(), + t.getUser().getEmail(), + t.getUser().getNickname() + ), + t.getCreatedAt(), + t.getModifiedAt() + )).toList(); + + return new PageImpl<>(dtoList, pageable, total != null ? total : 0); + } + + @Override + public Page findAllByWeatherAndDateRange(String weather, LocalDate startDate, LocalDate endDate, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + + if (weather != null) { + builder.and(todo.weather.eq(weather)); + } + if (startDate != null) { + builder.and(todo.modifiedAt.goe(startDate.atStartOfDay())); + } + if (endDate != null) { + builder.and(todo.modifiedAt.loe(endDate.atStartOfDay())); + } + List content = queryFactory + .select(Projections.constructor( + TodoResponse.class, + todo.id, + todo.title, + todo.contents, + todo.weather, + Projections.constructor(UserResponse.class, + todo.user.id, + todo.user.email, + todo.user.nickname + ), + todo.createdAt, + todo.modifiedAt + )) + .from(todo) + .join(todo.user) + .where(builder) + .orderBy(todo.modifiedAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 2. 전체 개수 쿼리 (count) + Long total = queryFactory + .select(todo.count()) + .from(todo) + .where(builder) + .fetchOne(); + + // 3. Page로 변환 + return new PageImpl<>(content, pageable, total != null ? total : 0); + } + + @Override + public Page findTodoSummary(Pageable pageable) { + + List content = queryFactory + .select(Projections.constructor( + TodoSummaryResponseDto.class, + todo.id, + todo.title, + manager.countDistinct(), + JPAExpressions.select(comment.count()) + .from(comment) + .where(comment.todo.id.eq(todo.id)) + )) + .from(todo) + .leftJoin(manager).on(manager.todo.id.eq(todo.id)) + .groupBy(todo.id, todo.title) + .orderBy(todo.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(todo.count()) + .from(todo) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0); + } + + } diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java index 667664637..572693d4b 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java @@ -5,10 +5,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.Optional; public interface TodoRepository extends JpaRepository, QTodoRepository { @@ -16,19 +12,4 @@ public interface TodoRepository extends JpaRepository, QTodoReposito Page findAllByOrderByModifiedAtDesc(Pageable pageable); - - @Query( - "SELECT t FROM Todo t " + - " JOIN FETCH t.user " + - " WHERE (:weather IS NULL OR t.weather = :weather)" + - "AND (: startDate IS NULL OR t.modifiedAt >= :startDate)" + - "AND (: endDate IS NULL OR t.modifiedAt <= :endDate)" + - "ORDER BY t.modifiedAt DESC" - ) - Page findAllByWeatherAndDateRange( - @Param("weather") String weather, - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - Pageable pageable - ); } diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 5c20d11fb..322e4a29d 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -7,6 +7,7 @@ import org.example.expert.domain.todo.dto.request.TodoSaveRequest; import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.dto.response.TodoSaveResponse; +import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; import org.example.expert.domain.todo.entity.Todo; import org.example.expert.domain.todo.repository.TodoRepository; import org.example.expert.domain.user.dto.response.UserResponse; @@ -17,7 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; +import java.time.LocalDate; @Service @RequiredArgsConstructor @@ -66,6 +67,7 @@ public Page getTodos(int page, int size) { )); } + // 쿼리dsl 사용 public TodoResponse getTodo(long todoId) { Todo todo = todoRepository.findByIdWithUser(todoId) .orElseThrow(() -> new InvalidRequestException("Todo not found")); @@ -83,15 +85,15 @@ public TodoResponse getTodo(long todoId) { ); } - public Page searchTodo(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) { + public Page searchTodo(int page, int size, String weather, LocalDate startDate, LocalDate endDate) { Pageable pageable = PageRequest.of(page - 1, size); if (startDate == null) { - startDate = LocalDateTime.of(1, 1, 1,0, 0); + startDate = LocalDate.of(1, 1, 1); } if (endDate == null) { - endDate = LocalDateTime.of(9999, 12, 31, 23, 59); + endDate = LocalDate.of(9999, 12, 31); } - Page todos = todoRepository.findAllByWeatherAndDateRange(weather,startDate,endDate, pageable); + Page todos = todoRepository.findAllByWeatherAndDateRange(weather, startDate, endDate, pageable); return todos.map(todo -> new TodoResponse( todo.getId(), todo.getTitle(), todo.getContents(), todo.getWeather(), @@ -101,4 +103,22 @@ public Page searchTodo(int page, int size, String weather, LocalDa )); } + public Page searchTitle(String keyword, Pageable pageable) { + Page todos = todoRepository.findByTitle(keyword, pageable); + return todos.map( + todoResponse -> new TodoResponse( + todoResponse.getId(), + todoResponse.getTitle(), + todoResponse.getContents(), + todoResponse.getWeather(), + new UserResponse(todoResponse.getId(), todoResponse.getUser().getEmail(), todoResponse.getUser().getNickname()), + todoResponse.getCreatedAt(), + todoResponse.getModifiedAt() + ) + ); + } + + public Page findTodoSummary(Pageable pageable) { + return todoRepository.findTodoSummary(pageable); + } } From 1e45456f706fc1275b83d004f9e35da681f8575c Mon Sep 17 00:00:00 2001 From: taehyeon Date: Tue, 1 Jul 2025 15:03:52 +0900 Subject: [PATCH 11/15] Lv 3 : 10 --- .../org/example/expert/config/security/SecurityConfig.java | 1 - .../expert/domain/manager/controller/ManagerController.java | 1 - .../example/expert/domain/todo/dto/response/TodoResponse.java | 3 --- 3 files changed, 5 deletions(-) diff --git a/src/main/java/org/example/expert/config/security/SecurityConfig.java b/src/main/java/org/example/expert/config/security/SecurityConfig.java index 4ca785961..02a112d3e 100644 --- a/src/main/java/org/example/expert/config/security/SecurityConfig.java +++ b/src/main/java/org/example/expert/config/security/SecurityConfig.java @@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor; import org.example.expert.config.JwtFilter; import org.example.expert.config.JwtUtil; -import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.enums.UserRole; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java index b2c6a9448..66f9812b7 100644 --- a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java +++ b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java @@ -2,7 +2,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.manager.dto.request.ManagerSaveRequest; import org.example.expert.domain.manager.dto.response.ManagerResponse; diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java index d72345380..3ec935e4b 100644 --- a/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoResponse.java @@ -1,9 +1,6 @@ package org.example.expert.domain.todo.dto.response; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; import org.example.expert.domain.user.dto.response.UserResponse; import java.time.LocalDateTime; From a4111f4bf34c4ba30fcc69ce90ecd4d289518b85 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Tue, 1 Jul 2025 17:15:02 +0900 Subject: [PATCH 12/15] Lv 3 : 11 --- .../aop/ManagerRegistrationLogAspect.java | 42 +++++++++++++++++++ .../org/example/expert/aop/entity/Log.java | 31 ++++++++++++++ .../expert/aop/repository/LogRepository.java | 10 +++++ .../expert/aop/service/LogService.java | 23 ++++++++++ 4 files changed, 106 insertions(+) create mode 100644 src/main/java/org/example/expert/aop/ManagerRegistrationLogAspect.java create mode 100644 src/main/java/org/example/expert/aop/entity/Log.java create mode 100644 src/main/java/org/example/expert/aop/repository/LogRepository.java create mode 100644 src/main/java/org/example/expert/aop/service/LogService.java diff --git a/src/main/java/org/example/expert/aop/ManagerRegistrationLogAspect.java b/src/main/java/org/example/expert/aop/ManagerRegistrationLogAspect.java new file mode 100644 index 000000000..6d4f567e6 --- /dev/null +++ b/src/main/java/org/example/expert/aop/ManagerRegistrationLogAspect.java @@ -0,0 +1,42 @@ +package org.example.expert.aop; + + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.example.expert.aop.service.LogService; +import org.example.expert.domain.common.dto.AuthUser; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Aspect +@Component +@RequiredArgsConstructor +public class ManagerRegistrationLogAspect { + + private final LogService logService; + private final HttpServletRequest request; // 요청 정보 추출을 위해 필요 + + // 매니저 등록 메서드 실행 "직후" 자동 실행 + @After("execution(* org.example.expert.domain.manager.service.ManagerService.saveManager(..))") + public void logManagerRegistration(JoinPoint joinPoint) { + // 예시: 첫 번째 파라미터로 User 객체가 들어온다고 가정 + Object[] args = joinPoint.getArgs(); + String nickname = null; + for (Object arg : args) { + if (arg instanceof AuthUser) { + nickname = ((AuthUser) arg).getNickname(); + break; + } + } + String requestUrl = request.getRequestURI(); + LocalDateTime now = LocalDateTime.now(); + String method = joinPoint.getSignature().getName(); + + // 로그 저장 (항상 별도 트랜잭션) + logService.saveLog(nickname, now, requestUrl, method); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/expert/aop/entity/Log.java b/src/main/java/org/example/expert/aop/entity/Log.java new file mode 100644 index 000000000..d797414cc --- /dev/null +++ b/src/main/java/org/example/expert/aop/entity/Log.java @@ -0,0 +1,31 @@ +package org.example.expert.aop.entity; + + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "log") +@Getter +@NoArgsConstructor +public class Log { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nickname; // 관리자 아이디 + private LocalDateTime requestTime; // 요청 시간 + private String requestUrl; // 요청 경로 + private String method; // 호출 메서드 + + public Log(String nickname, LocalDateTime requestTime, String requestUrl, String method) { + this.nickname = nickname; + this.requestTime = requestTime; + this.requestUrl = requestUrl; + this.method = method; + } +} diff --git a/src/main/java/org/example/expert/aop/repository/LogRepository.java b/src/main/java/org/example/expert/aop/repository/LogRepository.java new file mode 100644 index 000000000..b78e64c57 --- /dev/null +++ b/src/main/java/org/example/expert/aop/repository/LogRepository.java @@ -0,0 +1,10 @@ +package org.example.expert.aop.repository; + +import org.example.expert.aop.entity.Log; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LogRepository extends JpaRepository { + +} diff --git a/src/main/java/org/example/expert/aop/service/LogService.java b/src/main/java/org/example/expert/aop/service/LogService.java new file mode 100644 index 000000000..62f2f75be --- /dev/null +++ b/src/main/java/org/example/expert/aop/service/LogService.java @@ -0,0 +1,23 @@ +package org.example.expert.aop.service; + +import lombok.RequiredArgsConstructor; +import org.example.expert.aop.entity.Log; +import org.example.expert.aop.repository.LogRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class LogService { + + private final LogRepository logRepository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveLog(String userId, LocalDateTime requestTime, String requestUrl, String method) { + Log log = new Log(userId, requestTime, requestUrl, method); + logRepository.save(log); + } +} From 21a79ba665db4164bcf6739d36a41aa90a6fef4e Mon Sep 17 00:00:00 2001 From: taehyeon Date: Tue, 1 Jul 2025 17:21:22 +0900 Subject: [PATCH 13/15] Lv 3 : 11 --- src/main/java/org/example/expert/aop/service/LogService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/aop/service/LogService.java b/src/main/java/org/example/expert/aop/service/LogService.java index 62f2f75be..c93a54e29 100644 --- a/src/main/java/org/example/expert/aop/service/LogService.java +++ b/src/main/java/org/example/expert/aop/service/LogService.java @@ -16,8 +16,8 @@ public class LogService { private final LogRepository logRepository; @Transactional(propagation = Propagation.REQUIRES_NEW) - public void saveLog(String userId, LocalDateTime requestTime, String requestUrl, String method) { - Log log = new Log(userId, requestTime, requestUrl, method); + public void saveLog(String nickname, LocalDateTime requestTime, String requestUrl, String method) { + Log log = new Log(nickname, requestTime, requestUrl, method); logRepository.save(log); } } From 5b80c074094de2c0bc91dd717dcec4e86b922f29 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Wed, 2 Jul 2025 17:18:44 +0900 Subject: [PATCH 14/15] =?UTF-8?q?Lv=202=20:=209=20=EC=8B=9C=ED=81=90?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=EC=9D=B8=EC=A6=9D/=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/example/expert/config/JwtFilter.java | 2 -- .../example/expert/config/security/SecurityConfig.java | 10 +++------- .../domain/todo/repository/QTodoRepositoryImpl.java | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 26cf8ef21..487161652 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -72,8 +72,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha authUser, "", List.of(new SimpleGrantedAuthority("ROLE_" + userRole.name())) // 권한까지 부여 ); - - // 거를 contextholder에 대입 SecurityContextHolder.getContext().setAuthentication(authenticationToken); diff --git a/src/main/java/org/example/expert/config/security/SecurityConfig.java b/src/main/java/org/example/expert/config/security/SecurityConfig.java index 02a112d3e..9344b7cb5 100644 --- a/src/main/java/org/example/expert/config/security/SecurityConfig.java +++ b/src/main/java/org/example/expert/config/security/SecurityConfig.java @@ -7,7 +7,6 @@ import org.example.expert.domain.user.enums.UserRole; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -40,12 +39,9 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti // config로 url에 대한 인증/인가를 관리 .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/signup").permitAll() - .requestMatchers("/auth/signin").permitAll() - .requestMatchers("/todos").hasAnyRole(UserRole.USER.name(), UserRole.ADMIN.name()) // 유저랑 어드민 둘다 허용 - .requestMatchers("/todos/**").hasAnyRole(UserRole.USER.name(), UserRole.ADMIN.name()) - .requestMatchers(HttpMethod.PATCH, "/admin/users/").hasRole(UserRole.ADMIN.name()) - .anyRequest().denyAll() + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/admin/**").hasRole(UserRole.ADMIN.name()) + .anyRequest().authenticated() ) //필터 등록 diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java index f07ceaf70..18fa4d5ec 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java @@ -41,7 +41,7 @@ public Optional findByIdWithUser(Long id) { @Override public Page findByTitle(String keyword, Pageable pageable) { - // 1. 엔티티로 조회 + List content = queryFactory .selectFrom(todo) .where(todo.title.containsIgnoreCase(keyword)) From 3100cde7b84bbc53c1ac5bd8e2ac3cae390a3362 Mon Sep 17 00:00:00 2001 From: taehyeon Date: Wed, 2 Jul 2025 19:39:00 +0900 Subject: [PATCH 15/15] =?UTF-8?q?Lv=203=20:=2010=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EB=8B=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AuthUserArgumentResolver.java | 47 ------------- .../example/expert/config/FilterConfig.java | 22 ------ .../org/example/expert/config/WebConfig.java | 19 ----- .../todo/controller/TodoController.java | 19 ++--- .../dto/response/TodoSearchCondition.java | 19 +++++ .../todo/dto/response/TodoSearchDto.java | 14 ++++ .../dto/response/TodoSummaryResponseDto.java | 21 ------ .../todo/repository/QTodoRepository.java | 6 +- .../todo/repository/QTodoRepositoryImpl.java | 70 ++++++++----------- .../domain/todo/service/TodoService.java | 22 +----- 10 files changed, 73 insertions(+), 186 deletions(-) delete mode 100644 src/main/java/org/example/expert/config/AuthUserArgumentResolver.java delete mode 100644 src/main/java/org/example/expert/config/FilterConfig.java delete mode 100644 src/main/java/org/example/expert/config/WebConfig.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchCondition.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchDto.java delete mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java deleted file mode 100644 index 26ef47aa8..000000000 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.example.expert.config; - -import jakarta.servlet.http.HttpServletRequest; -import org.example.expert.domain.auth.exception.AuthException; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; -import org.example.expert.domain.user.enums.UserRole; -import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; -// -//public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { -// -// @Override -// public boolean supportsParameter(MethodParameter parameter) { -// boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null; -// boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class); -// -// // @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생 -// if (hasAuthAnnotation != isAuthUserType) { -// throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다."); -// } -// -// return hasAuthAnnotation; -// } -// -// @Override -// public Object resolveArgument( -// @Nullable MethodParameter parameter, -// @Nullable ModelAndViewContainer mavContainer, -// NativeWebRequest webRequest, -// @Nullable WebDataBinderFactory binderFactory -// ) { -// HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); -// -// // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 -// Long userId = (Long) request.getAttribute("userId"); -// String email = (String) request.getAttribute("email"); -// String nickname = (String) request.getAttribute("nickname"); -// UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); -// -// return new AuthUser(userId, email, nickname, userRole); -// } -//} diff --git a/src/main/java/org/example/expert/config/FilterConfig.java b/src/main/java/org/example/expert/config/FilterConfig.java deleted file mode 100644 index f86b74fa7..000000000 --- a/src/main/java/org/example/expert/config/FilterConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.example.expert.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -// -//@Configuration -//@RequiredArgsConstructor -//public class FilterConfig { -// -// private final JwtUtil jwtUtil; -// -// @Bean -// public FilterRegistrationBean jwtFilter() { -// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); -// registrationBean.setFilter(new JwtFilter(jwtUtil)); -// registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다. -// -// return registrationBean; -// } -//} diff --git a/src/main/java/org/example/expert/config/WebConfig.java b/src/main/java/org/example/expert/config/WebConfig.java deleted file mode 100644 index 5690611ea..000000000 --- a/src/main/java/org/example/expert/config/WebConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.expert.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import java.util.List; - -//@Configuration -//@RequiredArgsConstructor -//public class WebConfig implements WebMvcConfigurer { -// -// // ArgumentResolver 등록 -// @Override -// public void addArgumentResolvers(List resolvers) { -// resolvers.add(new AuthUserArgumentResolver()); -// } -//} diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index b0df65406..facc0701e 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -4,9 +4,7 @@ import lombok.RequiredArgsConstructor; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; -import org.example.expert.domain.todo.dto.response.TodoResponse; -import org.example.expert.domain.todo.dto.response.TodoSaveResponse; -import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; +import org.example.expert.domain.todo.dto.response.*; import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -55,17 +53,12 @@ public Page searchTodo( return todoService.searchTodo(page, size, weather, startDate, endDate); } - @GetMapping("/todos/title") - public Page searchTitle( - @RequestParam String keyword, + + @GetMapping("/todos/search2") + public Page searchTodos( + @ModelAttribute TodoSearchCondition condition, Pageable pageable ) { - return todoService.searchTitle(keyword, pageable); - } - - - @GetMapping("/todos/summary") - public Page findTodoSummary(Pageable pageable) { - return todoService.findTodoSummary(pageable); + return todoService.searchTodos(condition, pageable); } } diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchCondition.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchCondition.java new file mode 100644 index 000000000..6ea9cad6d --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchCondition.java @@ -0,0 +1,19 @@ +package org.example.expert.domain.todo.dto.response; + + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Setter +@NoArgsConstructor +public class TodoSearchCondition { + + private String title; + private String nickname; + private LocalDate startDate; + private LocalDate endDate; +} diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchDto.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchDto.java new file mode 100644 index 000000000..cab682ef8 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchDto.java @@ -0,0 +1,14 @@ +package org.example.expert.domain.todo.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TodoSearchDto { + + private String title; + private Long managerCount; + private Long commentCount; +} diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java deleted file mode 100644 index 939b4fb8c..000000000 --- a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSummaryResponseDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.example.expert.domain.todo.dto.response; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class TodoSummaryResponseDto { - - private Long todoId; - private String title; - private Long managerCount; - private Long commentCount; - - public TodoSummaryResponseDto(Long todoId, String title, Long managerCount, Long commentCount) { - this.todoId = todoId; - this.title = title; - this.managerCount = managerCount; - this.commentCount = commentCount; - } -} diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java index 311fbdec3..db5251704 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepository.java @@ -1,7 +1,8 @@ package org.example.expert.domain.todo.repository; import org.example.expert.domain.todo.dto.response.TodoResponse; -import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; +import org.example.expert.domain.todo.dto.response.TodoSearchCondition; +import org.example.expert.domain.todo.dto.response.TodoSearchDto; import org.example.expert.domain.todo.entity.Todo; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,7 +13,6 @@ public interface QTodoRepository { Optional findByIdWithUser(Long id); - Page findByTitle(String keyword, Pageable pageable); Page findAllByWeatherAndDateRange(String weather, LocalDate startDate, LocalDate endDate, Pageable pageable); - Page findTodoSummary(Pageable pageable); + Page searchTodos(TodoSearchCondition condition, Pageable pageable); } diff --git a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java index 18fa4d5ec..26c735d77 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/QTodoRepositoryImpl.java @@ -7,13 +7,15 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.example.expert.domain.todo.dto.response.TodoResponse; -import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; +import org.example.expert.domain.todo.dto.response.TodoSearchCondition; +import org.example.expert.domain.todo.dto.response.TodoSearchDto; import org.example.expert.domain.todo.entity.Todo; import org.example.expert.domain.user.dto.response.UserResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; import java.time.LocalDate; import java.util.List; @@ -39,41 +41,6 @@ public Optional findByIdWithUser(Long id) { .fetchOne()); } - @Override - public Page findByTitle(String keyword, Pageable pageable) { - - List content = queryFactory - .selectFrom(todo) - .where(todo.title.containsIgnoreCase(keyword)) - .orderBy(todo.modifiedAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - Long total = queryFactory - .select(todo.count()) - .from(todo) - .where(todo.title.containsIgnoreCase(keyword)) - .fetchOne(); - - List dtoList = content.stream() - .map(t -> new TodoResponse( - t.getId(), - t.getTitle(), - t.getContents(), - t.getWeather(), - new UserResponse( - t.getUser().getId(), - t.getUser().getEmail(), - t.getUser().getNickname() - ), - t.getCreatedAt(), - t.getModifiedAt() - )).toList(); - - return new PageImpl<>(dtoList, pageable, total != null ? total : 0); - } - @Override public Page findAllByWeatherAndDateRange(String weather, LocalDate startDate, LocalDate endDate, Pageable pageable) { BooleanBuilder builder = new BooleanBuilder(); @@ -122,12 +89,28 @@ public Page findAllByWeatherAndDateRange(String weather, LocalDate } @Override - public Page findTodoSummary(Pageable pageable) { + public Page searchTodos(TodoSearchCondition condition, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + + if (StringUtils.hasText(condition.getTitle())) { + builder.and(todo.title.containsIgnoreCase(condition.getTitle())); + } + + if (StringUtils.hasText(condition.getNickname())) { + builder.and(todo.managers.any().user.nickname.containsIgnoreCase(condition.getNickname())); + } - List content = queryFactory + // 생성일 범위 검색 (createdAt 기준) + if (condition.getStartDate() != null) { + builder.and(todo.createdAt.goe(condition.getStartDate().atStartOfDay())); + } + if (condition.getEndDate() != null) { + builder.and(todo.createdAt.loe(condition.getEndDate().atTime(23, 59, 59))); + } + + List content = queryFactory .select(Projections.constructor( - TodoSummaryResponseDto.class, - todo.id, + TodoSearchDto.class, todo.title, manager.countDistinct(), JPAExpressions.select(comment.count()) @@ -135,16 +118,19 @@ public Page findTodoSummary(Pageable pageable) { .where(comment.todo.id.eq(todo.id)) )) .from(todo) - .leftJoin(manager).on(manager.todo.id.eq(todo.id)) + .leftJoin(todo.managers, manager) + .where(builder) .groupBy(todo.id, todo.title) - .orderBy(todo.id.desc()) + .orderBy(todo.createdAt.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); + // 전체 개수 (for 페이징) Long total = queryFactory .select(todo.count()) .from(todo) + .where(builder) .fetchOne(); return new PageImpl<>(content, pageable, total != null ? total : 0); diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 322e4a29d..58d4e96f8 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -5,9 +5,7 @@ import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.common.exception.InvalidRequestException; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; -import org.example.expert.domain.todo.dto.response.TodoResponse; -import org.example.expert.domain.todo.dto.response.TodoSaveResponse; -import org.example.expert.domain.todo.dto.response.TodoSummaryResponseDto; +import org.example.expert.domain.todo.dto.response.*; import org.example.expert.domain.todo.entity.Todo; import org.example.expert.domain.todo.repository.TodoRepository; import org.example.expert.domain.user.dto.response.UserResponse; @@ -103,22 +101,8 @@ public Page searchTodo(int page, int size, String weather, LocalDa )); } - public Page searchTitle(String keyword, Pageable pageable) { - Page todos = todoRepository.findByTitle(keyword, pageable); - return todos.map( - todoResponse -> new TodoResponse( - todoResponse.getId(), - todoResponse.getTitle(), - todoResponse.getContents(), - todoResponse.getWeather(), - new UserResponse(todoResponse.getId(), todoResponse.getUser().getEmail(), todoResponse.getUser().getNickname()), - todoResponse.getCreatedAt(), - todoResponse.getModifiedAt() - ) - ); - } - public Page findTodoSummary(Pageable pageable) { - return todoRepository.findTodoSummary(pageable); + public Page searchTodos(TodoSearchCondition condition, Pageable pageable) { + return todoRepository.searchTodos(condition, pageable); } }