From a43b172cfcc6e7b03884668ddec837c7b1a3a80e Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:11:11 +0900 Subject: [PATCH 01/14] =?UTF-8?q?Transactional=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=82=B4=EC=9A=A9=20:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=EC=9D=B4=20=EC=9D=BD=EA=B8=B0=20=EC=A0=84=EC=9A=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=96=B4=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=97=86=EB=8B=A4=20?= =?UTF-8?q?Connection=20is=20read-only.=20Queries=20leading=20to=20data=20?= =?UTF-8?q?modification=20are=20not=20allowed=20@Transactional=20=EC=9D=84?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=A8=EC=9C=BC=EB=A1=9C=EC=8D=A8=20?= =?UTF-8?q?=EC=9D=BD=EA=B8=B0=20=EC=A0=84=EC=9A=A9=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EC=A0=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/expert/domain/todo/service/TodoService.java | 1 + 1 file changed, 1 insertion(+) 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..3ce628fb1 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 @@ -25,6 +25,7 @@ public class TodoService { private final TodoRepository todoRepository; private final WeatherClient weatherClient; + @Transactional public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) { User user = User.fromAuthUser(authUser); From a2ba2d439133ac9417c141a3cd7d2151c61059e1 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:11:09 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20jwt=20nickname=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AuthUserArgumentResolver.java | 5 +++-- .../org/example/expert/config/JwtFilter.java | 1 + .../org/example/expert/config/JwtUtil.java | 3 ++- .../auth/dto/request/SignupRequest.java | 2 ++ .../domain/auth/service/AuthService.java | 7 ++++--- .../comment/service/CommentService.java | 4 ++-- .../expert/domain/common/dto/AuthUser.java | 4 +++- .../manager/service/ManagerService.java | 4 ++-- .../domain/todo/service/TodoService.java | 6 +++--- .../user/dto/response/UserResponse.java | 4 +++- .../expert/domain/user/entity/User.java | 11 ++++++++--- .../domain/user/service/UserService.java | 2 +- src/main/resources/application.yml | 19 +++++++++++++++++++ 13 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/application.yml diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index db00211de..f83a375af 100644 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java @@ -36,11 +36,12 @@ public Object resolveArgument( ) { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 + // JwtFilter 에서 set 한 userId, email, userRole, nickname 값을 가져옴 Long userId = (Long) request.getAttribute("userId"); String email = (String) request.getAttribute("email"); UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); + String nickname = (String) request.getAttribute("nickname"); - return new AuthUser(userId, email, userRole); + return new AuthUser(userId, email, userRole, nickname); } } diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 03908abe1..b44e97f5b 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -60,6 +60,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); httpRequest.setAttribute("email", claims.get("email")); httpRequest.setAttribute("userRole", claims.get("userRole")); + httpRequest.setAttribute("nickname", claims.get("nickname")); 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 07e0a2c7c..230bec1b8 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, UserRole userRole) { + public String createToken(Long userId, String email, UserRole userRole, String nickname) { Date date = new Date(); return BEARER_PREFIX + @@ -42,6 +42,7 @@ public String createToken(Long userId, String email, UserRole userRole) { .setSubject(String.valueOf(userId)) .claim("email", email) .claim("userRole", userRole) + .claim("nickname", nickname) .setExpiration(new Date(date.getTime() + TOKEN_TIME)) .setIssuedAt(date) // 발급일 .signWith(key, signatureAlgorithm) // 암호화 알고리즘 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..084e64b34 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 @@ -17,4 +17,6 @@ public class SignupRequest { private String password; @NotBlank private String userRole; + @NotBlank + private String nickname; } 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..2a5bc1a35 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 @@ -38,11 +38,12 @@ public SignupResponse signup(SignupRequest signupRequest) { User newUser = new User( signupRequest.getEmail(), encodedPassword, - userRole + userRole, + signupRequest.getNickname() ); User savedUser = userRepository.save(newUser); - String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole); + String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole, savedUser.getNickname()); 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.getUserRole(),user.getNickname()); 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..af2ab22a3 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 @@ -9,10 +9,12 @@ public class AuthUser { private final Long id; private final String email; private final UserRole userRole; + private final String nickname; - public AuthUser(Long id, String email, UserRole userRole) { + public AuthUser(Long id, String email, UserRole userRole, String nickname) { this.id = id; this.email = email; this.userRole = userRole; + this.nickname = nickname; } } 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/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 3ce628fb1..61007f79a 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 @@ -44,7 +44,7 @@ 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()) ); } @@ -58,7 +58,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() )); @@ -75,7 +75,7 @@ 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() ); 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..0d504aa76 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 @@ -21,20 +21,25 @@ public class User extends Timestamped { @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String password, UserRole userRole) { + @Column(nullable = true) + private String nickname; + + public User(String email, String password, UserRole userRole, String nickname) { this.email = email; this.password = password; this.userRole = userRole; + this.nickname = nickname; } - private User(Long id, String email, UserRole userRole) { + private User(Long id, String email, UserRole userRole, String nickname) { this.id = id; this.email = email; this.userRole = userRole; + this.nickname = nickname; } public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole()); + return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname()); } 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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..141200635 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,19 @@ +jwt: + secret: + key: afd456af34s435s62dfa46ads5f425as4562asfd2564fads5462asdf4562afds2456 + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/spring_plus + username: root + password: 6745 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: true + dialect: org.hibernate.dialect.MySQLDialect \ No newline at end of file From d29a90ec608af80b204122db2edf0a120fb0ff53 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:19:29 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix:=20todo=20Search=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=82=A0=EC=94=A8,=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=9A=94=EC=B2=AD=20jpql=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EB=8F=84=20=EC=A0=9C=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/controller/TodoController.java | 7 ++-- .../todo/repository/TodoRepository.java | 16 +++++++-- .../domain/todo/service/TodoService.java | 36 +++++++++++++++++-- 3 files changed, 52 insertions(+), 7 deletions(-) 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..025c043c1 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 @@ -29,9 +29,12 @@ public ResponseEntity saveTodo( @GetMapping("/todos") public ResponseEntity> getTodos( @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String weather, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate ) { - return ResponseEntity.ok(todoService.getTodos(page, size)); + return ResponseEntity.ok(todoService.getTodos(page, size, weather, startDate, endDate)); } @GetMapping("/todos/{todoId}") 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..34ad2ec53 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,15 +7,25 @@ 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 { - @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); + + // 날씨가 없을경우 + // 날짜없는 경우 + // @Param 메소드와 연결을 위함 + @Query("select t from Todo t " + + "WHERE (:weather IS NULL OR t.weather = :weather) " + + "AND (:startDate IS NULL OR t.modifiedAt >= :startDate) " + + "AND (:endDate IS NULL OR t.modifiedAt <= :endDate)") + Page findTodosByConditions(Pageable pageable, + @Param("weather") String weather, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } 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 61007f79a..422c9d011 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 @@ -16,6 +16,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.format.DateTimeFormatter; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; @Service @RequiredArgsConstructor @@ -24,6 +29,8 @@ public class TodoService { private final TodoRepository todoRepository; private final WeatherClient weatherClient; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + @Transactional public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) { @@ -48,10 +55,35 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ ); } - public Page getTodos(int page, int size) { + public Page getTodos(int page, int size, String weather, String startDateStr, String endDateStr) { Pageable pageable = PageRequest.of(page - 1, size); - Page todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); + LocalDate startDate = null; + if (startDateStr != null && !startDateStr.isEmpty()) { + startDate = LocalDate.parse(startDateStr, DATE_FORMATTER); + } + + LocalDate endDate = null; + if (endDateStr != null && !endDateStr.isEmpty()) { + endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + } + + LocalDateTime startDateTime = null; + if (startDate != null) { + startDateTime = startDate.atStartOfDay(); + } + + LocalDateTime endDateTime = null; + if (endDate != null) { + endDateTime = endDate.atTime(LocalTime.MAX); + } + + Page todos = todoRepository.findTodosByConditions( + pageable, + weather, + startDateTime, + endDateTime + ); return todos.map(todo -> new TodoResponse( todo.getId(), From 365393186f078077cf4764b60766d12f511a1374 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:37:21 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20Lv1-4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=98=88=EC=99=B8=EA=B0=80=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=B4=EC=95=BC=ED=95=98=EB=8A=94=EB=8D=B0=20?= =?UTF-8?q?=EC=A0=95=EC=83=81=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=9E=88=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/todo/controller/TodoControllerTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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..a0047abe1 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", UserRole.USER, "하룻강아지"); User user = User.fromAuthUser(authUser); - UserResponse userResponse = new UserResponse(user.getId(), user.getEmail()); + UserResponse userResponse = new UserResponse(user.getId(), user.getEmail(), authUser.getNickname()); TodoResponse response = new TodoResponse( todoId, title, @@ -61,7 +61,7 @@ class TodoControllerTest { @Test void todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() throws Exception { // given - long todoId = 1L; + long todoId = 5L; // when when(todoService.getTodo(todoId)) @@ -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 d216445d41dc57af20d249e326fd8e528c987df3 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:25:52 +0900 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20Lv1-5=20AOP=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=EC=8B=9C=EC=A0=90=20=EC=88=98=EC=A0=95=20aop=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=20api=20=ED=98=B8=EC=B6=9C=20=EC=A0=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/expert/aop/AdminAccessLoggingAspect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java index c90e8c792..b2cc2fddb 100644 --- a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java +++ b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java @@ -4,8 +4,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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 +18,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.UserController.getUser(..))") public void logAfterChangeUserRole(JoinPoint joinPoint) { String userId = String.valueOf(request.getAttribute("userId")); String requestUrl = request.getRequestURI(); From 49ef6f630019133769d84ab46fa08b664266c298 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:48:32 +0900 Subject: [PATCH 06/14] =?UTF-8?q?fix:=20Lv2-6=20todo=20cascade=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 5621c64a13f602ea4001dfd5660dfd05e9ea8436 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:07:41 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20Lv2-7=20N+1=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20FETCH=20=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/domain/comment/repository/CommentRepository.java | 2 +- 1 file changed, 1 insertion(+), 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..ecb21ce56 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,6 @@ public interface CommentRepository extends JpaRepository { - @Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId") + @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId") List findByTodoIdWithUser(@Param("todoId") Long todoId); } From c1d31b29161d37e849bda680ccaf001575dbbbea Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:39:28 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20Lv2-8=20todo=20=EB=8B=A8=EA=B1=B4?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20QueryDSL=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20j?= =?UTF-8?q?pql=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC=20entityManager=20?= =?UTF-8?q?=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++ .../example/expert/config/QueryDslConfig.java | 22 +++++++++++++ .../todo/repository/TodoCustomRepository.java | 9 ++++++ .../repository/TodoCustomRepositoryImpl.java | 32 +++++++++++++++++++ .../todo/repository/TodoRepository.java | 10 +++--- 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/example/expert/config/QueryDslConfig.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java diff --git a/build.gradle b/build.gradle index a7fd3e706..cc64183d1 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,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/java/org/example/expert/config/QueryDslConfig.java b/src/main/java/org/example/expert/config/QueryDslConfig.java new file mode 100644 index 000000000..2575bf500 --- /dev/null +++ b/src/main/java/org/example/expert/config/QueryDslConfig.java @@ -0,0 +1,22 @@ +package org.example.expert.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + private EntityManager entityManager; + + public QueryDslConfig(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} + diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java new file mode 100644 index 000000000..4a23b2575 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java @@ -0,0 +1,9 @@ +package org.example.expert.domain.todo.repository; + +import org.example.expert.domain.todo.entity.Todo; + +import java.util.Optional; + +public interface TodoCustomRepository { + Optional findByIdWithUser(Long todoId); +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java new file mode 100644 index 000000000..fd39dce9d --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java @@ -0,0 +1,32 @@ +package org.example.expert.domain.todo.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.example.expert.domain.todo.entity.QTodo; +import org.example.expert.domain.todo.entity.Todo; +import org.example.expert.domain.user.entity.QUser; + + +import java.util.Optional; + +public class TodoCustomRepositoryImpl implements TodoCustomRepository{ + + private final JPAQueryFactory jpaQueryFactory; + + public TodoCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.jpaQueryFactory = jpaQueryFactory; + } + + @Override + public Optional findByIdWithUser(Long todoId) { + + QTodo todo = QTodo.todo; + QUser user = QUser.user; + + return Optional.ofNullable(jpaQueryFactory + .selectFrom(todo) + .leftJoin(todo.user,user) + .fetchJoin() + .where(todo.id.eq(todoId)) + .fetchOne()); + } +} \ No newline at end of file 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 34ad2ec53..2482c3e58 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,12 +10,12 @@ import java.time.LocalDateTime; import java.util.Optional; -public interface TodoRepository extends JpaRepository { +public interface TodoRepository extends JpaRepository, TodoCustomRepository { - @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 " + +// "LEFT JOIN t.user " + +// "WHERE t.id = :todoId") +// Optional findByIdWithUser(@Param("todoId") Long todoId); // 날씨가 없을경우 // 날짜없는 경우 From 82a73d016c17e01be300a188b8e8a5aa05fa297a Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:49:31 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20Lv2-9=20Spring=20Security=201.=20b?= =?UTF-8?q?uild.gradle=20:=20Spring=20Security=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80=202.=20JwtFilt?= =?UTF-8?q?er=20:=20Spring=20Security=EC=9D=98=20OncePerRequestFilter?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=B2=B4=20->=20FilterConfig=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=203.=20@Auth=20:=20Spring=20Security=EC=9D=98=20@Auth?= =?UTF-8?q?enticationPrincipal=EC=9D=84=20=EC=82=AC=EC=9A=A9=20->=20WebCon?= =?UTF-8?q?fig,=20AuthUserArgumentResolver=20=EC=A0=9C=EA=B1=B0=204.=20Sec?= =?UTF-8?q?urityConfig:=20JWT=20=EC=9D=B8=EC=A6=9D=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20=20=20=E2=86=92=20"/auth/**"?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=EB=8A=94=20=EC=9D=B8=EC=A6=9D=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=ED=97=88=EC=9A=A9,=20=20=20=20=20"/admin/**"?= =?UTF-8?q?=EB=8A=94=20ADMIN=20=EA=B6=8C=ED=95=9C=EB=A7=8C=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +- .../config/AuthUserArgumentResolver.java | 47 ----------- .../example/expert/config/FilterConfig.java | 22 ----- .../org/example/expert/config/JwtFilter.java | 84 +++++++++---------- .../example/expert/config/SecurityConfig.java | 46 ++++++++++ .../org/example/expert/config/WebConfig.java | 19 ----- .../comment/controller/CommentController.java | 4 +- .../expert/domain/common/annotation/Auth.java | 11 --- .../expert/domain/common/dto/AuthUser.java | 25 +++++- .../manager/controller/ManagerController.java | 6 +- .../todo/controller/TodoController.java | 4 +- .../user/controller/UserController.java | 4 +- .../expert/domain/user/enums/UserRole.java | 8 ++ 13 files changed, 131 insertions(+), 154 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 create mode 100644 src/main/java/org/example/expert/config/SecurityConfig.java delete mode 100644 src/main/java/org/example/expert/config/WebConfig.java delete mode 100644 src/main/java/org/example/expert/domain/common/annotation/Auth.java diff --git a/build.gradle b/build.gradle index cc64183d1..f0a2f02cc 100644 --- a/build.gradle +++ b/build.gradle @@ -42,11 +42,14 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - //QueryDsl + // 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" + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' } tasks.named('test') { 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 f83a375af..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, nickname 값을 가져옴 - Long userId = (Long) request.getAttribute("userId"); - String email = (String) request.getAttribute("email"); - UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); - String nickname = (String) request.getAttribute("nickname"); - - return new AuthUser(userId, email, userRole, nickname); - } -} 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 34cb4088a..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/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index b44e97f5b..1dc81887b 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -10,38 +10,34 @@ 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.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Slf4j @RequiredArgsConstructor -public class JwtFilter implements Filter { +// Been 등록 +@Component +//public class JwtFilter implements Filter { +public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; @Override - public void init(FilterConfig filterConfig) throws ServletException { - Filter.super.init(filterConfig); - } + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + ) throws ServletException, IOException { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - HttpServletResponse httpResponse = (HttpServletResponse) response; + // auth 경로 Spring Security에서 설정 - String url = httpRequest.getRequestURI(); - - if (url.startsWith("/auth")) { - chain.doFilter(request, response); - return; - } - - String bearerJwt = httpRequest.getHeader("Authorization"); + String bearerJwt = request.getHeader("Authorization"); if (bearerJwt == null) { - // 토큰이 없는 경우 400을 반환합니다. - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다."); return; } @@ -51,45 +47,45 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // JWT 유효성 검사와 claims 추출 Claims claims = jwtUtil.extractClaims(jwt); if (claims == null) { - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다."); return; } - UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class)); + // 클레임에서 사용자 정보 추출 + Long id = Long.parseLong(claims.getSubject()); + String email = claims.get("email", String.class); + String userRoleString = claims.get("userRole", String.class); + String nickname = claims.get("nickname", String.class); - httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); - httpRequest.setAttribute("email", claims.get("email")); - httpRequest.setAttribute("userRole", claims.get("userRole")); - httpRequest.setAttribute("nickname", claims.get("nickname")); + UserRole userRole = UserRole.valueOf(userRoleString); - if (url.startsWith("/admin")) { - // 관리자 권한이 없는 경우 403을 반환합니다. - if (!UserRole.ADMIN.equals(userRole)) { - httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다."); - return; - } - chain.doFilter(request, response); - return; - } + // authUser 가져온 값들을 주입 + AuthUser authUser = new AuthUser(id, email, userRole, nickname); + + // Spring Security의 Authentication 객체 생성 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + authUser, null, authUser.getAuthorities()); + + // SecurityContextHolder에 Authentication 객체를 설정합니다. + SecurityContextHolder.getContext().setAuthentication(authentication); - chain.doFilter(request, response); + filterChain.doFilter(request, response); } catch (SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); } catch (ExpiredJwtException e) { log.error("Expired JWT token, 만료된 JWT token 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); + } catch (IllegalArgumentException e) { // UserRole.valueOf() 실패 등 추가 예외 처리 + log.error("JWT claims parsing error or invalid user role.", e); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 클레임 처리 오류 또는 유효하지 않은 사용자 역할입니다."); } catch (Exception e) { - log.error("Internal server error", e); - httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.error("Internal server error during JWT processing.", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "JWT 처리 중 서버 내부 오류가 발생했습니다."); } } - - @Override - public void destroy() { - Filter.super.destroy(); - } } diff --git a/src/main/java/org/example/expert/config/SecurityConfig.java b/src/main/java/org/example/expert/config/SecurityConfig.java new file mode 100644 index 000000000..72720c2c4 --- /dev/null +++ b/src/main/java/org/example/expert/config/SecurityConfig.java @@ -0,0 +1,46 @@ +package org.example.expert.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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 JwtFilter jwtFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // CSRF 보호 비활성화 (JWT 토큰 기반 인증 사용) + .csrf(AbstractHttpConfigurer::disable) + // 세션 관리 정책: STATELESS (서버에 세션 저장 안함) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 접근 권한 설정 + .authorizeHttpRequests(authorize -> authorize + //허용 경로지정 + // "/auth"로 시작하는 모든 요청은 인증 없이 허용 (회원가입, 로그인) + .requestMatchers("/auth/**").permitAll() + // "/admin"으로 시작하는 요청은 'ADMIN' 역할을 가진 사용자만 허용 + .requestMatchers("/admin/**").hasRole("ADMIN") + // 나머지는 인증 필요 + .anyRequest().authenticated() + ); + + // JwtFilter를 Spring Security의 필터 체인에 추가 + // 사용자 인증 정보가 JWT를 통해 우선순위 설정 + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} 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 adff06b82..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/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/common/annotation/Auth.java b/src/main/java/org/example/expert/domain/common/annotation/Auth.java deleted file mode 100644 index 770061855..000000000 --- a/src/main/java/org/example/expert/domain/common/annotation/Auth.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.expert.domain.common.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -public @interface Auth { -} 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 af2ab22a3..137ef378f 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 @@ -2,9 +2,15 @@ import lombok.Getter; import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; @Getter -public class AuthUser { +public class AuthUser implements UserDetails { private final Long id; private final String email; @@ -17,4 +23,21 @@ public AuthUser(Long id, String email, UserRole userRole, String nickname) { this.userRole = userRole; this.nickname = nickname; } + + // 권환 반환 + @Override + public Collection getAuthorities() { + return Collections.singleton(userRole.getGrantedAuthority()); + } + + @Override + public String getPassword() { + return email; + } + + @Override + public String getUsername() { + return null; + } + } 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..918ffe28c 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,13 +2,13 @@ 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; 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 +21,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 +35,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 025c043c1..5080ac0fb 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,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.todo.dto.request.TodoSaveRequest; import org.example.expert.domain.todo.dto.response.TodoResponse; @@ -10,6 +9,7 @@ import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -20,7 +20,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)); 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..f737e84cf 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 @@ -1,12 +1,12 @@ package org.example.expert.domain.user.controller; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.user.dto.request.UserChangePasswordRequest; 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 +21,7 @@ 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/main/java/org/example/expert/domain/user/enums/UserRole.java b/src/main/java/org/example/expert/domain/user/enums/UserRole.java index 6fe177896..ec173c25b 100644 --- a/src/main/java/org/example/expert/domain/user/enums/UserRole.java +++ b/src/main/java/org/example/expert/domain/user/enums/UserRole.java @@ -1,6 +1,8 @@ package org.example.expert.domain.user.enums; import org.example.expert.domain.common.exception.InvalidRequestException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Arrays; @@ -13,4 +15,10 @@ public static UserRole of(String role) { .findFirst() .orElseThrow(() -> new InvalidRequestException("유효하지 않은 UerRole")); } + + // 시큐리티 권한 반환을 위한 메서드 + public GrantedAuthority getGrantedAuthority() { + return new SimpleGrantedAuthority("ROLE_" + this.name()); + } + } From ad15fd74a93407277e808dc41ec91de4d3adf0c2 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:23:43 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20Lv2-9=20Spring=20Security=20addFil?= =?UTF-8?q?terBefore=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/example/expert/config/JwtFilter.java | 3 +-- .../java/org/example/expert/config/SecurityConfig.java | 8 +++----- 2 files changed, 4 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 1dc81887b..5224507b6 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -23,7 +23,6 @@ @RequiredArgsConstructor // Been 등록 @Component -//public class JwtFilter implements Filter { public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; @@ -88,4 +87,4 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "JWT 처리 중 서버 내부 오류가 발생했습니다."); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/expert/config/SecurityConfig.java b/src/main/java/org/example/expert/config/SecurityConfig.java index 72720c2c4..06afc5109 100644 --- a/src/main/java/org/example/expert/config/SecurityConfig.java +++ b/src/main/java/org/example/expert/config/SecurityConfig.java @@ -23,6 +23,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http // CSRF 보호 비활성화 (JWT 토큰 기반 인증 사용) .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화 + .formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화 // 세션 관리 정책: STATELESS (서버에 세션 저장 안함) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -35,11 +37,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/admin/**").hasRole("ADMIN") // 나머지는 인증 필요 .anyRequest().authenticated() - ); - - // JwtFilter를 Spring Security의 필터 체인에 추가 - // 사용자 인증 정보가 JWT를 통해 우선순위 설정 - http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + ).addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } From a6c5c6431b696defbd0dae5036772135ce39911a Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:32:16 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20Lv3-10=20QueryDSL=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EA=B2=80=EC=83=89=20api=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/controller/TodoController.java | 10 +++ .../request/SearchTodoConditionRequest.java | 12 +++ .../todo/dto/response/SearchTodoResponse.java | 13 +++ .../todo/repository/TodoCustomRepository.java | 6 ++ .../repository/TodoCustomRepositoryImpl.java | 86 +++++++++++++++++++ .../domain/todo/service/TodoService.java | 8 ++ 6 files changed, 135 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/todo/dto/request/SearchTodoConditionRequest.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/SearchTodoResponse.java 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 5080ac0fb..11727fe21 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 @@ -3,11 +3,14 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.domain.todo.dto.request.SearchTodoConditionRequest; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; +import org.example.expert.domain.todo.dto.response.SearchTodoResponse; import org.example.expert.domain.todo.dto.response.TodoResponse; 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.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -41,4 +44,11 @@ public ResponseEntity> getTodos( public ResponseEntity getTodo(@PathVariable long todoId) { return ResponseEntity.ok(todoService.getTodo(todoId)); } + + @GetMapping("/secrch") + public ResponseEntity> searchTodo( + SearchTodoConditionRequest conditionRequest, + Pageable pageable) { + return ResponseEntity.ok(todoService.searchTodo(conditionRequest, pageable)); + } } diff --git a/src/main/java/org/example/expert/domain/todo/dto/request/SearchTodoConditionRequest.java b/src/main/java/org/example/expert/domain/todo/dto/request/SearchTodoConditionRequest.java new file mode 100644 index 000000000..eeb57a2a0 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/request/SearchTodoConditionRequest.java @@ -0,0 +1,12 @@ +package org.example.expert.domain.todo.dto.request; + +import lombok.Getter; +import java.time.LocalDate; + +@Getter +public class SearchTodoConditionRequest { + private String keyword; + private String Nickname; + private LocalDate startDate; + private LocalDate endDate; +} diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/SearchTodoResponse.java b/src/main/java/org/example/expert/domain/todo/dto/response/SearchTodoResponse.java new file mode 100644 index 000000000..2d63a6056 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/response/SearchTodoResponse.java @@ -0,0 +1,13 @@ +package org.example.expert.domain.todo.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class SearchTodoResponse { + private String title; + private Long managerCount; + private Long commentCount; + +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java index 4a23b2575..31d7fafe3 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepository.java @@ -1,9 +1,15 @@ package org.example.expert.domain.todo.repository; +import org.example.expert.domain.todo.dto.request.SearchTodoConditionRequest; +import org.example.expert.domain.todo.dto.response.SearchTodoResponse; import org.example.expert.domain.todo.entity.Todo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import java.util.Optional; public interface TodoCustomRepository { Optional findByIdWithUser(Long todoId); + + Page searchByCondition(SearchTodoConditionRequest conditionRequest, Pageable pageable); } diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java index fd39dce9d..dd64b9f78 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java @@ -1,11 +1,22 @@ package org.example.expert.domain.todo.repository; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import org.example.expert.domain.comment.entity.QComment; +import org.example.expert.domain.manager.entity.QManager; +import org.example.expert.domain.todo.dto.request.SearchTodoConditionRequest; +import org.example.expert.domain.todo.dto.response.SearchTodoResponse; import org.example.expert.domain.todo.entity.QTodo; import org.example.expert.domain.todo.entity.Todo; import org.example.expert.domain.user.entity.QUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; public class TodoCustomRepositoryImpl implements TodoCustomRepository{ @@ -29,4 +40,79 @@ public Optional findByIdWithUser(Long todoId) { .where(todo.id.eq(todoId)) .fetchOne()); } + + @Override + public Page searchByCondition( + SearchTodoConditionRequest conditionRequest, + Pageable pageable) { + QTodo todo = QTodo.todo; + QUser user = QUser.user; + QComment comment = QComment.comment; + QManager manager = QManager.manager; + + List content = jpaQueryFactory + .select(Projections.constructor(SearchTodoResponse.class, + todo.title, + manager.id.countDistinct(), + comment.id.countDistinct() + )) + .from(todo) + .leftJoin(todo.managers, manager) + .leftJoin(manager.user,user) + .leftJoin(todo.comments, comment) + .where( + titleContains(conditionRequest.getKeyword()), + managerNicknameContains(conditionRequest.getNickname()), + createdDateBetween(conditionRequest.getStartDate(), conditionRequest.getEndDate()) + ) + .groupBy(todo.id) + .orderBy(todo.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = jpaQueryFactory + .select(todo.id.countDistinct()) + .from(todo) + .leftJoin(todo.managers, manager) + .leftJoin(manager.user, user) + .where( + titleContains(conditionRequest.getKeyword()), + managerNicknameContains(conditionRequest.getNickname()), + createdDateBetween(conditionRequest.getStartDate(), conditionRequest.getEndDate()) + ) + .fetchOne(); + + long totalCount = 0; + if (total != null) { + totalCount = total; + } + + return new PageImpl<>(content, pageable, totalCount); + } + + private BooleanExpression titleContains(String keyword) { + if (keyword != null || !keyword.isBlank()) { + return null; + } + return QTodo.todo.title.containsIgnoreCase(keyword); + } + + private BooleanExpression managerNicknameContains(String nickname) { + if (nickname == null || nickname.isBlank()) { + return null; + } + return QManager.manager.user.nickname.containsIgnoreCase(nickname); + } + + private BooleanExpression createdDateBetween(LocalDate start, LocalDate end) { + if (start != null && end != null) { + return QTodo.todo.createdAt.between(start.atStartOfDay(), end.plusDays(1).atStartOfDay()); + } else if (start != null) { + return QTodo.todo.createdAt.goe(start.atStartOfDay()); + } else if (end != null) { + return QTodo.todo.createdAt.loe(end.plusDays(1).atStartOfDay()); + } + return null; + } } \ No newline at end of file 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 422c9d011..d9d189136 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 @@ -4,7 +4,9 @@ import org.example.expert.client.WeatherClient; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.common.exception.InvalidRequestException; +import org.example.expert.domain.todo.dto.request.SearchTodoConditionRequest; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; +import org.example.expert.domain.todo.dto.response.SearchTodoResponse; import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.dto.response.TodoSaveResponse; import org.example.expert.domain.todo.entity.Todo; @@ -112,4 +114,10 @@ public TodoResponse getTodo(long todoId) { todo.getModifiedAt() ); } + + public Page searchTodo( + SearchTodoConditionRequest conditionRequest, + Pageable pageable) { + return todoRepository.searchByCondition(conditionRequest, pageable); + } } From e1e484a21633a945bb28d38271da183d3367e88c Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:21:04 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20Lv1-1~5=20README=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=B8=EC=8B=9D=20=EB=B0=8F=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=20&=20=ED=95=B4=EA=B2=B0=EB=B0=A9=EC=95=88=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/README.md b/README.md index e19f8e392..1663075ab 100644 --- a/README.md +++ b/README.md @@ -1 +1,108 @@ # SPRING PLUS + + + + + +### 1-1. 문제 인식 및 정의 +- /todos API를 호출하여 할 일을 저장하려고 할 때, 다음과 같은 에러가 발생합니다 +``` +jakarta.servlet.ServletException: Request processing failed: +org.springframework.orm.jpa.JpaSystemException: +could not execute statement [Connection is read-only. +Queries leading to data modification are not allowed] +``` +- 상단에 @Transactional(readOnly = true)어노테이션으로 인하여 DB 연결이 read-only 상태이기 때문에, INSERT, UPDATE와 같은 쓰기 작업이 차단되어 발생한 오류입니다. +- 해당 API는 데이터를 저장하는 쓰기 작업이므로, 트랜잭션이 read-write 모드로 설정되어 있어야 합니다. + + +### 1-1. 해결 방안 +- @Transactional을 서비스 메서드에 올바르게 적용 + - saveTodo() 같이 수정이 필요한 메서드에 @Transactional을 추가하여 쓰기 가능한 트랜잭션을 활성화 + + +### 1-1. 해결 완료 +``` +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TodoService { + private final TodoRepository todoRepository; + private final WeatherClient weatherClient; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + + @Transactional + public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) { + User user = User.fromAuthUser(authUser); + ... + Todo savedTodo = todoRepository.save(newTodo); + ... + } +} +``` +- 이제 saveTodo()가 실행될 때 Spring이 트랜잭션을 열고, 쓰기 작업도 허용되기 때문에 + 정상적으로 할 일이 저장됩니다. + +### 1-2. 문제 인식 및 정의 +- 현재 서비스에서 JWT를 발급할 때, 사용자 식별 정보로 userId, email, userRole만 포함하고 있으며, 프론트엔드 화면에서 nickname을 함께 표시하고자 하는 요구사항이 생겼습니다. +- 기존 JWT에는 nickname 정보가 포함되어 있지 않기 때문에, 클라이언트는 서버에서 nickname을 다시 조회하거나 요청을 추가로 보내야 하는 불편이 있습니다. + +### 1-2. 해결 방안 +1. User 엔티티 및 요청 객체 수정 + - User 테이블에 nickname 컬럼을 추가 + - 닉네임은 중복 가능하며, 별도의 제약은 두지 않음 + - 회원가입 및 응답 객체에 nickname 추가 +2. JWT 토큰 생성 시 nickname 추가 + - JwtUtil.createToken() 메서드의 파라미터에 nickname 추가 + - .claim("nickname", nickname) 으로 JWT에 nickname 추가 +3. JwtFilter에서 nickname 추출 +4. AuthUser 클래스 수정 + - nickname 컬럼을 추가 +5. 각종 응답값에 nickname 추가 + +### 1-2. 해결 완료 +- User.nickname 일관되게 반영 +- JWT 생성시 nickname 추가 + + +### 1-3. 문제 인식 및 정의 +- 기존의 할 일 목록 조회 API (/todos)에 기능추가 + - 날씨 조건으로 검색 + - 수정일 기간 검색 + +### 1-3. 해결 방안 +- @GetMapping 이므로 날씨, 시작일, 종료일 모두 바디가 아닌 @RequestParam 값으로 각각 요청 +- QueryDSL 사용하여 추후에 유지보수하기 용이함 + +### 1-4. 문제 인식 및 정의 +- 존재하지 않는 할일인 경우 InvalidRequestException이 발생하도록 처리하고 있습니다.
+하지만 컨트롤러 테스트 메서드 todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다()는 예외가 발생하였음에도 불구하고 응답 코드로 HttpStatus.OK(200) 되어있습니다. + +### 1-4. 해결 방안 +``` +// 변경 전 +.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())) + ``` +- InvalidRequestException 처리에 따라 반환되는 HTTP 상태 코드 400 (BAD_REQUEST) 와 일치하도록 수정 + +### 1-5. 문제 인식 및 정의 +- 현재 AOP 클래스인 AdminAccessLoggingAspect는 @After 어노테이션으로 UserController의 getUser 메소드 실행 시점에만 동작하도록 설정되어 있습니다.
+ 하지만 여기서의 요구하는 의도는 changeUserRole() 메소드 실행 시점에서 관리자가 접근로그를 남기려고 합니다.
+ AOP 포인트컷이 잘못 설정되어 있어 원하는 시점에 로그가 기록되지 않는 문제가 있었습니다. + +### 1-5. 해결 방안 +- AOP 포인트컷 수정 +``` +// 변경 전 +@After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") +// 변경 후 +@Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))") +``` +- UserAdminController.changeUserRole() 메소드 전 실행으로 수정 \ No newline at end of file From 079be6a566f4eae1350e27b451d5b16c9754e943 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:13:43 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fiz:=20Lv1-6=20~=202-8=20README=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=9D=B8=EC=8B=9D=20=EB=B0=8F=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20&=20=ED=95=B4=EA=B2=B0=EB=B0=A9=EC=95=88=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 127 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1663075ab..3bfb840c2 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ ### 1-1. 문제 인식 및 정의 -- /todos API를 호출하여 할 일을 저장하려고 할 때, 다음과 같은 에러가 발생합니다 +- /todos API를 호출하여 할 일을 저장하려고 할 때 다음과 같은 에러가 발생합니다 ``` jakarta.servlet.ServletException: Request processing failed: org.springframework.orm.jpa.JpaSystemException: could not execute statement [Connection is read-only. Queries leading to data modification are not allowed] ``` -- 상단에 @Transactional(readOnly = true)어노테이션으로 인하여 DB 연결이 read-only 상태이기 때문에, INSERT, UPDATE와 같은 쓰기 작업이 차단되어 발생한 오류입니다. -- 해당 API는 데이터를 저장하는 쓰기 작업이므로, 트랜잭션이 read-write 모드로 설정되어 있어야 합니다. +- 상단에 @Transactional(readOnly = true)어노테이션으로 인하여 DB 연결이 read-only 상태이기 때문에 INSERT, UPDATE와 같은 쓰기 작업이 차단되어 발생한 오류입니다. +- 해당 API는 데이터를 저장하는 쓰기 작업이므로 트랜잭션이 read-write 모드로 설정되어 있어야 합니다. ### 1-1. 해결 방안 @@ -41,17 +41,20 @@ public class TodoService { } } ``` -- 이제 saveTodo()가 실행될 때 Spring이 트랜잭션을 열고, 쓰기 작업도 허용되기 때문에 - 정상적으로 할 일이 저장됩니다. +- saveTodo()가 실행될 때 Spring이 트랜잭션을 열고, 쓰기 작업도 허용되기 때문에 정상적으로 할 일이 저장됩니다. + +
+
+ ### 1-2. 문제 인식 및 정의 -- 현재 서비스에서 JWT를 발급할 때, 사용자 식별 정보로 userId, email, userRole만 포함하고 있으며, 프론트엔드 화면에서 nickname을 함께 표시하고자 하는 요구사항이 생겼습니다. -- 기존 JWT에는 nickname 정보가 포함되어 있지 않기 때문에, 클라이언트는 서버에서 nickname을 다시 조회하거나 요청을 추가로 보내야 하는 불편이 있습니다. +- 현재 서비스에서 JWT를 발급할 , 사용자 식별 정보로 userId, email, userRole만 포함하고 있으며 프론트엔드 화면에서 nickname을 함께 표시하고자 하는 요구사항이 생겼습니다. +- 기존 JWT에는 nickname 정보가 포함되어 있지 않기 때문에 클라이언트는 서버에서 nickname을 다시 조회하거나 요청을 추가로 보내야 하는 불편이 있습니다. ### 1-2. 해결 방안 1. User 엔티티 및 요청 객체 수정 - User 테이블에 nickname 컬럼을 추가 - - 닉네임은 중복 가능하며, 별도의 제약은 두지 않음 + - 닉네임은 중복 가능하고 별도의 제약은 두지 않음 - 회원가입 및 응답 객체에 nickname 추가 2. JWT 토큰 생성 시 nickname 추가 - JwtUtil.createToken() 메서드의 파라미터에 nickname 추가 @@ -65,6 +68,8 @@ public class TodoService { - User.nickname 일관되게 반영 - JWT 생성시 nickname 추가 +
+
### 1-3. 문제 인식 및 정의 - 기존의 할 일 목록 조회 API (/todos)에 기능추가 @@ -75,6 +80,9 @@ public class TodoService { - @GetMapping 이므로 날씨, 시작일, 종료일 모두 바디가 아닌 @RequestParam 값으로 각각 요청 - QueryDSL 사용하여 추후에 유지보수하기 용이함 +
+
+ ### 1-4. 문제 인식 및 정의 - 존재하지 않는 할일인 경우 InvalidRequestException이 발생하도록 처리하고 있습니다.
하지만 컨트롤러 테스트 메서드 todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다()는 예외가 발생하였음에도 불구하고 응답 코드로 HttpStatus.OK(200) 되어있습니다. @@ -103,6 +111,115 @@ public class TodoService { // 변경 전 @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") // 변경 후 -@Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))") +@Before("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") +``` + +
+
+ +### 2-6. 문제 인식 및 정의 +- 할 일을 작성하여 저장시 유저가 자동적으로 담당자로 등로되어야 합니다. 하지만 Manager 엔티티가 Todo와 연관 관계를 맺고 있음에도 불구하고 Todo 저장 시 Manager는 저장되지 않아 문제가 발생했습니다. + +### 2-6. 해결 방안 +- CascadeType.PERSIST 적용 + - @OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)를 통해 Todo 저장 시 함께 등록된 Manager 엔티티도 함께 저장 + +### 2-6. 해결 완료 +``` +@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST) +private List managers = new ArrayList<>(); +``` + +
+
+ +### 2-7. 문제 인식 및 정의 +- N+1 문제 + - CommentController.getComments() API 호출 시 각 Comment 마다 User를 별도 쿼리로 조회하여 N+1 문제 발생 + +### 2-7 해결 방안 +``` +JOIN FETCH : 연관 객체도 한 번의 쿼리로 즉시 로딩 + - 단점 : 페이징이 불가능 한 경우도 있음 +EntityGraph : 간결한 문법으로 fetch join 처리 + - 단점 : 복잡한 쿼리에서는 사용이 불가능 +``` + +### 2-7. 해결 완료 +``` + @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId") + List findByTodoIdWithUser(@Param("todoId") Long todoId); +``` +- JOIN FETCH 를 사용하여 Comment 와 연관된 User 를 한 번의 쿼리로 함께 조회 + +
+
+ +### 2-8. 문제 인식 및 정의 +- TodoService.getTodo 메소드 내 todoRepository.findByIdWithUser(todoId) 호출 시 기존 JPQL로 작성된 쿼리가 N+1 문제를 발생시킬 수 있습니다. + +### 2-8 해결 방안 +- 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" ``` -- UserAdminController.changeUserRole() 메소드 전 실행으로 수정 \ No newline at end of file +- JPAQueryFactory 빈을 등록하여 QueryDSL 쿼리를 사용할 수 있도록 설정 +``` +@Configuration +public class QueryDslConfig { + + private final EntityManager entityManager; + + public QueryDslConfig(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} +``` +- QueryDSL을 사용하여 구현할 메소드 +``` +public interface TodoCustomRepository { +Optional findByIdWithUser(Long todoId); +} +``` +- TodoCustomRepository 인터페이스를 구현 +- JPAQueryFactory를 사용하여 Todo와 User를 fetchJoin()으로 함께 조회 +``` +public class TodoCustomRepositoryImpl implements TodoCustomRepository { + private final JPAQueryFactory jpaQueryFactory; + public TodoCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.jpaQueryFactory = jpaQueryFactory; + } + + @Override + public Optional findByIdWithUser(Long todoId) { + // Q클래스 인스턴스 생성 + // todo와 user 필드 접근 ? + QTodo todo = QTodo.todo; + QUser user = QUser.user; + + // QueryDSL 쿼리 작성: todo와 user를 fetchJoin하여 함께 조회 + return Optional.ofNullable(jpaQueryFactory + .selectFrom(todo) + .leftJoin(todo.user, user) + .fetchJoin() + .where(todo.id.eq(todoId)) + .fetchOne()); + } +} +``` +- JpaRepository를 상속받는 TodoRepository 인터페이스에 TodoCustomRepository 인터페이스를 추가로 상속 + +``` +public interface TodoRepository extends JpaRepository, TodoCustomRepository { +``` +### 2-8. 해결 완료 +- Todo를 조회할 때 연관된 User 정보도 fetchJoin을 통해 한번의 쿼리로 가져오게되어 N+1 문제를 해결 + From c89187dead97d438d9bb2d241802d0dd8708c0b3 Mon Sep 17 00:00:00 2001 From: GuDaeWoong <39939767+GuDaeWoong@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:48:27 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fiz:=20Lv3-10=20README=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=B8=EC=8B=9D=20=EB=B0=8F=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=20&=20=ED=95=B4=EA=B2=B0=EB=B0=A9=EC=95=88=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 166 ++++++++++++++++++ .../expert/domain/common/dto/AuthUser.java | 4 +- .../todo/controller/TodoController.java | 2 +- .../repository/TodoCustomRepositoryImpl.java | 2 +- 4 files changed, 170 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3bfb840c2..7c7fcc426 100644 --- a/README.md +++ b/README.md @@ -223,3 +223,169 @@ public interface TodoRepository extends JpaRepository, TodoCustomRep ### 2-8. 해결 완료 - Todo를 조회할 때 연관된 User 정보도 fetchJoin을 통해 한번의 쿼리로 가져오게되어 N+1 문제를 해결 +
+
+ +### 2-9. 문제 인식 및 정의 +- Filter와 Argument Resolver를 사용하여 접근 권한 및 사용자 권한 관리를 하고 있습니다. +- Spring Security로 전환하고, 기존의 접근 권한 및 사용자 권한 기능은 그대로 유지하되 권한 관리는 Spring Security의 기능을 활용하는 것 +- 기존의 토큰 기반 인증 방식(JWT)은 그대로 유지 + +### 2-9. 해결 방안 +- Spring Security 의존성 추가 +``` + implementation 'org.springframework.boot:spring-boot-starter-security' +``` +- SecurityConfig 생성 + - CSRF 비활성화 + - BasicAuthenticationFilter 비활성화 + - UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화 + - 세션 관리 정책: STATELESS (서버에 세션 저장 안함) + - 인가 규칙 설정 + +``` +@Configuration +@EnableWebSecurity //시큐리티 활성화 +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtFilter jwtFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // CSRF 보호 비활성화 (JWT 토큰 기반 인증 사용) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화 + .formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화 + // 세션 관리 정책: STATELESS (서버에 세션 저장 안함) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 접근 권한 설정 + .authorizeHttpRequests(authorize -> authorize + //허용 경로지정 + // "/auth"로 시작하는 모든 요청은 인증 없이 허용 (회원가입, 로그인) + .requestMatchers("/auth/**").permitAll() + // "/admin"으로 시작하는 요청은 'ADMIN' 역할을 가진 사용자만 허용 + .requestMatchers("/admin/**").hasRole("ADMIN") + // 나머지는 인증 필요 + .anyRequest().authenticated() + ).addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} +``` + +- JwtFilter를 OncePerRequestFilter로 변경 + - 기존 Filter 인터페이스를 상속받던 JwtFilter를 Spring Security가 제공하는 OncePerRequestFilter로 변경 + * HTTP 요청당 한 번만 필터를 실행하도록 보장하고 Spring Security의 필터 체인에 통합하기 좋다고 합니다. + * -> FilterConfig 제거 +``` +@Slf4j +@RequiredArgsConstructor +@Component // Spring Bean으로 등록 +public class JwtFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String bearerJwt = request.getHeader("Authorization"); + + if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다."); + return; + } + + try { + String jwt = bearerJwt.substring(7); // "Bearer " 제거 + Claims claims = jwtUtil.extractClaims(jwt); + + if (claims == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다."); + return; + } + + // 클레임에서 사용자 정보 추출 + Long id = Long.parseLong(claims.getSubject()); + String email = claims.get("email", String.class); + String userRoleString = claims.get("userRole", String.class); + String nickname = claims.get("nickname", String.class); + UserRole userRole = UserRole.valueOf(userRoleString); + + // AuthUser 객체 생성 + AuthUser authUser = new AuthUser(id, email, userRole, nickname); + + // Spring Security의 Authentication 객체 생성 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + authUser, null, authUser.getAuthorities()); + + // SecurityContextHolder에 Authentication 객체를 설정 + SecurityContextHolder.getContext().setAuthentication(authentication); + + ... + } +} +``` +- AuthUser에 UserDetails 인터페이스 구현 + - AuthUser 클래스가 Spring Security에서 사용자 상세 정보를 가져올 수 있게 구현 + - +``` +@Getter +public class AuthUser implements UserDetails { + + ... + + // 권환 반환 + @Override + public Collection getAuthorities() { + return Collections.singleton(userRole.getGrantedAuthority()); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return email; + } +} +``` + +- @AuthenticationPrincipal 어노테이션 활용 + - 기존 @Auth 커스텀 Argument Resolver 대신해서 Spring Security에서 제공하는 @AuthenticationPrincipal 어노테이션을 사용해서 인증된 값을 주입받을 수 있다 + - -> WebConfig, AuthUserArgumentResolver 제거 + + +
+
+ + +### 3-10. 문제 인식 및 정의 +- 검색 api 작성 +- 검색 조건 + - 검색 키워드로 일정의 제목을 검색 + - 제목은 부분적으로 일치해도 검색이 가능 + - 일정의 생성일 범위로 검색 + - 일정을 생성일 최신순으로 정렬해주세요. + - 담당자의 닉네임으로도 검색이 가능 + - 닉네임은 부분적으로 일치해도 검색이 가능 +- 다음의 내용을 포함해서 검색 결과를 반환 + - 일정에 대한 모든 정보가 아닌, 제목만 반환 + - 해당 일정의 담당자 수를 반환 + - 해당 일정의 총 댓글 개수를 반환 +- 검색 결과는 페이징 처리되어 반환 + +### 3-10. 해결 방안 +- SearchTodoConditionRequest + - keyword, nickname, startDate, endDate +- BooleanExpression 사용 + - QueryDSL에서 WHERE 조건을 표현하는 객체 + - .where() 절에 조건을 걸고 싶을 때 사용 가능 + 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 137ef378f..cde9a2032 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 @@ -32,12 +32,12 @@ public Collection getAuthorities() { @Override public String getPassword() { - return email; + return null; } @Override public String getUsername() { - return null; + return email; } } 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 11727fe21..955214ec7 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 @@ -45,7 +45,7 @@ public ResponseEntity getTodo(@PathVariable long todoId) { return ResponseEntity.ok(todoService.getTodo(todoId)); } - @GetMapping("/secrch") + @GetMapping("/search") public ResponseEntity> searchTodo( SearchTodoConditionRequest conditionRequest, Pageable pageable) { diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java index dd64b9f78..c3c16c5e1 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoCustomRepositoryImpl.java @@ -92,7 +92,7 @@ public Page searchByCondition( } private BooleanExpression titleContains(String keyword) { - if (keyword != null || !keyword.isBlank()) { + if (keyword != null || keyword.isBlank()) { return null; } return QTodo.todo.title.containsIgnoreCase(keyword);