From 796ee01799532c8bed20b4ab1b22e3dda129ae0f Mon Sep 17 00:00:00 2001 From: cherry pick Date: Wed, 1 Apr 2026 15:59:00 +0900 Subject: [PATCH 01/16] =?UTF-8?q?lv1=20:=20Transactional=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80(on=20?= =?UTF-8?q?saveTodo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/expert/domain/todo/service/TodoService.java | 4 +++- 1 file changed, 3 insertions(+), 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..6b31dd5c5 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,12 +19,14 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional(readOnly = true) // <- 클래스 전체가 읽기 전용으로 되어 있어서 saveTodo에 @Transactional만 추가하면 되겠군 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 f383ac5f98200f5057dc829d107d77965a86c21d Mon Sep 17 00:00:00 2001 From: cherry pick Date: Wed, 1 Apr 2026 16:21:26 +0900 Subject: [PATCH 02/16] =?UTF-8?q?lv2=20:=20user=20Entity=EC=99=80=20jwt=20?= =?UTF-8?q?Util=EC=97=90=20nickname=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/example/expert/config/JwtUtil.java | 3 ++- .../expert/domain/auth/dto/request/SignupRequest.java | 2 ++ .../expert/domain/auth/service/AuthService.java | 10 ++++++---- .../org/example/expert/domain/user/entity/User.java | 10 +++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/example/expert/config/JwtUtil.java b/src/main/java/org/example/expert/config/JwtUtil.java index 07e0a2c7c..59ec13e9e 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..ded662902 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..ae68a5cb1 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,13 @@ public SignupResponse signup(SignupRequest signupRequest) { User newUser = new User( signupRequest.getEmail(), encodedPassword, - userRole + userRole, + signupRequest.getNickname() // <- nickname 추가 ); 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()); + // token에 닉네임 추가 return new SignupResponse(bearerToken); } @@ -56,8 +58,8 @@ 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()); + // token에 닉네임 추가 return new SigninResponse(bearerToken); } } 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..cddd9489e 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 @@ -20,21 +20,25 @@ public class User extends Timestamped { private String password; @Enumerated(EnumType.STRING) private UserRole userRole; + @Column(unique = true) + private String nickname; - public User(String email, String password, UserRole userRole) { + 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) { From 68d4301e674b2d3cc94d526fc7fe27881a6015a1 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 16:01:53 +0900 Subject: [PATCH 03/16] =?UTF-8?q?lv3-1=20:=20JPQL=EB=AC=B8=20=EB=8B=A4?= =?UTF-8?q?=EC=A4=91=EC=9C=BC=EB=A1=9C=20=EC=9E=91=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/repository/TodoRepository.java | 22 +++++++++++++++++-- .../domain/todo/service/TodoService.java | 19 ++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) 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..2d4f6ec57 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 @@ -1,5 +1,6 @@ package org.example.expert.domain.todo.repository; +import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.entity.Todo; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -7,15 +8,32 @@ 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 { + + // : Todo -> TodoResponse로 쿄쿄 @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") - Page findAllByOrderByModifiedAtDesc(Pageable pageable); + Page findAllByOrderByModifiedAtDesc(Pageable pageable); // user 기준 수정일 @Query("SELECT t FROM Todo t " + "LEFT JOIN t.user " + "WHERE t.id = :todoId") - Optional findByIdWithUser(@Param("todoId") Long todoId); + Optional findByIdWithUser(@Param("todoId") Long todoId); //user + + + // Todo로 받아서 처리할려고 했는데 TodoResponse로 받아도 상관 없을듯? + @Query("select t from Todo t where t.weather = :weather and t.modifiedAt between :start and :end") // 날씨랑 날짜 검색 조건이 있을 때 + Page findAllByWeatherAndDate(String weather, LocalDateTime start, LocalDateTime end, Pageable pageable); + + // 날씨만 있을 때 : Todo -> TodoResponse로 쿄쿄 + Page findAllByWeather(String weather, Pageable pageable); + + // 기간만 있을 때 : Todo -> TodoResponse로 쿄쿄 + @Query("select t from Todo t where t.modifiedAt between :start and :end") + Page findAllByModifiedAtBetween(LocalDateTime start, LocalDateTime end, 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 6b31dd5c5..6107757de 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(readOnly = true) // <- 클래스 전체가 읽기 전용으로 되어 있어서 saveTodo에 @Transactional만 추가하면 되겠군 @@ -49,10 +51,23 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ ); } - public Page getTodos(int page, int size) { + // 파라미터랑 조건문 추가해주기(일단 내가 많이했던거 써보기~) + public Page getTodos(String weather, LocalDateTime start, LocalDateTime end, int page, int size) { Pageable pageable = PageRequest.of(page - 1, size); - Page todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); + // todos 변수 설정하는데 빈 페이지로 초기화 해주기 -> 로직을 다 태우지 못하면 초기화 할 수 있도록.. + Page todos = Page.empty(); + + if(weather != null && start !=null) { + todos = todoRepository.findAllByWeatherAndDate(weather, start, end, pageable); + } else if (weather != null) { + todos = todoRepository.findAllByWeather(weather, pageable); + } else if (start != null && end != null) { + todos = todoRepository.findAllByModifiedAtBetween(start, end, pageable); + } else { + todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); // 조건없이 검색했을 때 -> 기존 코드를 if문 최종으로 넘기기 + } + return todos.map(todo -> new TodoResponse( todo.getId(), From c946bfba9ad090108c52a2dcc79de969ec5dfe9f Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 16:20:43 +0900 Subject: [PATCH 04/16] =?UTF-8?q?Lv3-2=20:=20=EB=8B=A8=EC=9D=BC=20JPQL?= =?UTF-8?q?=EB=AC=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/repository/TodoRepository.java | 44 +++++++++++++------ .../domain/todo/service/TodoService.java | 26 ++++++----- 2 files changed, 45 insertions(+), 25 deletions(-) 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 2d4f6ec57..999ef2838 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 @@ -13,10 +13,28 @@ public interface TodoRepository extends JpaRepository { + // + @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) " + + // 수정일 내림차순으로 + "order by t.modifiedAt desc") + Page findByCondition( + // 검토 결과 @Param을 추가하게 됬는데 @Param을 사용하면 JPQL변수와 파라미터 이름이 다르더라고 @Param으로 보완 가능 :D + @Param("weather") String weather, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + Pageable pageable); - // : Todo -> TodoResponse로 쿄쿄 - @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") - Page findAllByOrderByModifiedAtDesc(Pageable pageable); // user 기준 수정일 + + + +// // : Todo -> TodoResponse로 쿄쿄 -> if문도 길어지므로 JPQL문 하나로 써보기 +// @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") +// Page findAllByOrderByModifiedAtDesc(Pageable pageable); // user 기준 수정일 @Query("SELECT t FROM Todo t " + "LEFT JOIN t.user " + @@ -24,16 +42,16 @@ public interface TodoRepository extends JpaRepository { Optional findByIdWithUser(@Param("todoId") Long todoId); //user - // Todo로 받아서 처리할려고 했는데 TodoResponse로 받아도 상관 없을듯? - @Query("select t from Todo t where t.weather = :weather and t.modifiedAt between :start and :end") // 날씨랑 날짜 검색 조건이 있을 때 - Page findAllByWeatherAndDate(String weather, LocalDateTime start, LocalDateTime end, Pageable pageable); - - // 날씨만 있을 때 : Todo -> TodoResponse로 쿄쿄 - Page findAllByWeather(String weather, Pageable pageable); - - // 기간만 있을 때 : Todo -> TodoResponse로 쿄쿄 - @Query("select t from Todo t where t.modifiedAt between :start and :end") - Page findAllByModifiedAtBetween(LocalDateTime start, LocalDateTime end, Pageable pageable); +// // Todo로 받아서 처리할려고 했는데 TodoResponse로 받아도 상관 없을듯? -> if문도 길어지므로 JPQL문 하나로 써보기 +// @Query("select t from Todo t where t.weather = :weather and t.modifiedAt between :start and :end") // 날씨랑 날짜 검색 조건이 있을 때 +// Page findAllByWeatherAndDate(String weather, LocalDateTime start, LocalDateTime end, Pageable pageable); +// +// // 날씨만 있을 때 : Todo -> TodoResponse로 쿄쿄 -> if문도 길어지므로 JPQL문 하나로 써보기 +// Page findAllByWeather(String weather, Pageable pageable); +// +// // 기간만 있을 때 : Todo -> TodoResponse로 쿄쿄 -> if문도 길어지므로 JPQL문 하나로 써보기 +// @Query("select t from Todo t where t.modifiedAt between :start and :end") +// Page findAllByModifiedAtBetween(LocalDateTime start, LocalDateTime end, 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 6107757de..c715c9155 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 @@ -51,23 +51,25 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ ); } - // 파라미터랑 조건문 추가해주기(일단 내가 많이했던거 써보기~) + // 1. 파라미터랑 조건문 추가해주기(일단 내가 많이했던거 써보기~) + // 2, 단일 JPQL 만들기 public Page getTodos(String weather, LocalDateTime start, LocalDateTime end, int page, int size) { Pageable pageable = PageRequest.of(page - 1, size); - // todos 변수 설정하는데 빈 페이지로 초기화 해주기 -> 로직을 다 태우지 못하면 초기화 할 수 있도록.. - Page todos = Page.empty(); + // 2, 단일 JPQL 만들기 + Page todos = todoRepository.findByCondition(weather, start, end, pageable); - if(weather != null && start !=null) { - todos = todoRepository.findAllByWeatherAndDate(weather, start, end, pageable); - } else if (weather != null) { - todos = todoRepository.findAllByWeather(weather, pageable); - } else if (start != null && end != null) { - todos = todoRepository.findAllByModifiedAtBetween(start, end, pageable); - } else { - todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); // 조건없이 검색했을 때 -> 기존 코드를 if문 최종으로 넘기기 - } + // 1. 파라미터랑 조건문 추가해주기(일단 내가 많이했던거 써보기~) +// if(weather != null && start !=null) { +// todos = todoRepository.findAllByWeatherAndDate(weather, start, end, pageable); +// } else if (weather != null) { +// todos = todoRepository.findAllByWeather(weather, pageable); +// } else if (start != null && end != null) { +// todos = todoRepository.findAllByModifiedAtBetween(start, end, pageable); +// } else { +// todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); // 조건없이 검색했을 때 -> 기존 코드를 if문 최종으로 넘기기 +// } return todos.map(todo -> new TodoResponse( todo.getId(), From cb29843b2a7b50a4915c374a33a6df84c4219d33 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 16:35:19 +0900 Subject: [PATCH 05/16] =?UTF-8?q?lv2=20:=20jwt=ED=8C=8C=ED=8A=B8=20authUse?= =?UTF-8?q?r=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/expert/domain/common/dto/AuthUser.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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..7616d1759 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; } } From e5b6d64e63cb4622fde08873af8012e1c3a372a1 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 16:48:19 +0900 Subject: [PATCH 06/16] =?UTF-8?q?fix=20:=20lv2;=20authUser=20=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95,=20lv3;=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/expert/config/AuthUserArgumentResolver.java | 5 +++-- .../expert/domain/todo/controller/TodoController.java | 7 ++++++- .../expert/domain/todo/controller/TodoControllerTest.java | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index db00211de..e559dfd57 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 값을 가져옴 + 닉네임 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/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index eed1a1b46..1fc6484c9 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 @@ -12,6 +12,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; + @RestController @RequiredArgsConstructor public class TodoController { @@ -28,10 +30,13 @@ public ResponseEntity saveTodo( @GetMapping("/todos") public ResponseEntity> getTodos( + @RequestParam(required = false) String weather, + @RequestParam(required = false)LocalDateTime start, + @RequestParam(required = false) LocalDateTime end, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size ) { - return ResponseEntity.ok(todoService.getTodos(page, size)); + return ResponseEntity.ok(todoService.getTodos(weather, start, end, page, size)); } @GetMapping("/todos/{todoId}") 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..5c40528a1 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,7 +35,7 @@ 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, "nickname"); User user = User.fromAuthUser(authUser); UserResponse userResponse = new UserResponse(user.getId(), user.getEmail()); TodoResponse response = new TodoResponse( From 50bc18dc61d8918e4c157331fdd214a917f3ed13 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 17:26:07 +0900 Subject: [PATCH 07/16] =?UTF-8?q?LV4=20:=20=EC=84=9C=EB=B2=84=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20jwt=20=ED=82=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 5 +++++ .../expert/domain/todo/controller/TodoControllerTest.java | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..2b96c63f6 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,5 @@ +jwt: + secret: + key: 776974682d7769742d616e642d63616e646f722d616e642d68756d6f722d616e642d656d70617468794c4f5645 + + 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 5c40528a1..dde47e4da 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 @@ -69,9 +69,8 @@ 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()) //isOk -> isBadRequest로 바꾸기 + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) // HttpStatus.OK.name()) <- 우린 괜찮지 않으니 빼버리기~ .andExpect(jsonPath("$.message").value("Todo not found")); } } From 95f81eca380a17352ed5e1112c1a85df7ac08f19 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 17:40:11 +0900 Subject: [PATCH 08/16] =?UTF-8?q?lv5=20:=20aop=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/aop/AdminAccessLoggingAspect.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 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..8b8258153 100644 --- a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java +++ b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java @@ -1,4 +1,4 @@ -package org.example.expert.aop; +package org.example.expert.aop; //check import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -6,20 +6,28 @@ 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; @Slf4j -@Aspect -@Component +@Aspect // check +@Component // check @RequiredArgsConstructor public class AdminAccessLoggingAspect { private final HttpServletRequest request; - @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") - public void logAfterChangeUserRole(JoinPoint joinPoint) { + + // after? around? before? + // usercontroller에서 getuser 메서드 이후에 동작 +// @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") + //UserAdminController 클래스의 changeUserRole() 메소드가 실행 전 동작해야해요 <- 요구사항을 충족시키기 위해 + // 실행 전이므로 before로 바꿔주고 경로(UserAdminController.changeUserRole)도 바꿔줘야 할 듯? + @Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))") + //메서드명도 바꿈 after -> before + public void logBeforeChangeUserRole(JoinPoint joinPoint) { String userId = String.valueOf(request.getAttribute("userId")); String requestUrl = request.getRequestURI(); LocalDateTime requestTime = LocalDateTime.now(); From d832c0a87db85f568ff1b26dc5d0d8d74d4d6bc2 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 17:49:51 +0900 Subject: [PATCH 09/16] =?UTF-8?q?lv6=20:=20cascade=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/expert/domain/manager/entity/Manager.java | 2 +- .../java/org/example/expert/domain/todo/entity/Todo.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/domain/manager/entity/Manager.java b/src/main/java/org/example/expert/domain/manager/entity/Manager.java index 3e0dde234..05f83eeb7 100644 --- a/src/main/java/org/example/expert/domain/manager/entity/Manager.java +++ b/src/main/java/org/example/expert/domain/manager/entity/Manager.java @@ -24,6 +24,6 @@ public class Manager { public Manager(User user, Todo todo) { this.user = user; - this.todo = todo; + this.todo = todo; //check~ } } 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..27b224d0f 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,11 @@ public class Todo extends Timestamped { @OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE) private List comments = new ArrayList<>(); - @OneToMany(mappedBy = "todo") + + // ALL을 쓰면 담당자 정보 전부 타노스 당함 + // PERSIST 담당자 정보는 남아 있음. + // 이력 관리를 하는게 이득이라면 PERSIST가 낫지 않을까? <- 추후 db분석에 쓰일 수도 있으니.. + @OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST) private List managers = new ArrayList<>(); public Todo(String title, String contents, String weather, User user) { From 19f7840b49d791b24802f9d981ad62e036a1cb4f Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 18:25:06 +0900 Subject: [PATCH 10/16] =?UTF-8?q?lv8=20:=20=EC=BF=BC=EB=A6=AC=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20fetch=20=EC=B6=94=EA=B0=80=EB=A1=9C=20n+1=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/comment/controller/CommentController.java | 3 +++ .../domain/comment/repository/CommentRepository.java | 5 ++++- src/main/resources/application.yml | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) 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..1b18c0ece 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 @@ -28,6 +28,9 @@ public ResponseEntity saveComment( return ResponseEntity.ok(commentService.saveComment(authUser, todoId, commentSaveRequest)); } + // 로그 분석 : 댓글조회시 마다 사용자 정보를 달라고함 ㄷㄷ + // entity와 레포 점검 ㄱㄱ~~ + @GetMapping("/todos/{todoId}/comments") public ResponseEntity> getComments(@PathVariable long todoId) { return ResponseEntity.ok(commentService.getComments(todoId)); 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..0d574c5c8 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,9 @@ public interface CommentRepository extends JpaRepository { - @Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId") + //로그 정보를 보니 comment를 조회할때 + //음.. 그냥 join을 붙이면 user 객체안에 해당하는 정보들이 비어있는 체로 넘어오면서 그 비어있는걸 조회할려고 db무한왕복해야되서 n+1이 발생함. + // fetch만 붙여줌 ㅇㅇ + @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId") List findByTodoIdWithUser(@Param("todoId") Long todoId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2b96c63f6..2c5dda8c7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,3 +3,14 @@ jwt: key: 776974682d7769742d616e642d63616e646f722d616e642d68756d6f722d616e642d656d70617468794c4f5645 +spring: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + use_sql_comments: true +logging: + level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace \ No newline at end of file From 8a46034de78056308ee6076d04801656ee4c8549 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Thu, 2 Apr 2026 19:59:08 +0900 Subject: [PATCH 11/16] =?UTF-8?q?Lv8=20:=20QueryDSL=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 28 ++++++++ .../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/QueryConfig.java | 20 ++++++ .../todo/repository/CustomTodoRepository.java | 9 +++ .../todo/repository/TodoRepository.java | 15 ++-- .../todo/repository/TodoRepositoryImpl.java | 35 ++++++++++ .../domain/todo/service/TodoService.java | 7 ++ 11 files changed, 386 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/QueryConfig.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java create mode 100644 src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java diff --git a/build.gradle b/build.gradle index a7fd3e706..68afdeb8b 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,34 @@ 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 + // 1. QueryDSL 라이브러리 + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + + // 2. QueryDSL용 어노테이션 프로세서 (컴파일 시 Q클래스 생성) + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" +} + +def querydslDir = "src/main/generated" + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) +} + +clean { + delete file(querydslDir) } 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/QueryConfig.java b/src/main/java/org/example/expert/config/QueryConfig.java new file mode 100644 index 000000000..6519fe95f --- /dev/null +++ b/src/main/java/org/example/expert/config/QueryConfig.java @@ -0,0 +1,20 @@ +package org.example.expert.config; + + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java new file mode 100644 index 000000000..8427b5a93 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.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 CustomTodoRepository { + Optional findByIdWithUser(Long 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 999ef2838..297c1da0d 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 @@ -1,6 +1,5 @@ package org.example.expert.domain.todo.repository; -import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.entity.Todo; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,9 +8,9 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; -import java.util.Optional; -public interface TodoRepository extends JpaRepository { +//커스텀 레포 상속시켜주기 +public interface TodoRepository extends JpaRepository, CustomTodoRepository { // @Query("select t from Todo t " + @@ -36,10 +35,12 @@ Page findByCondition( // @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") // Page findAllByOrderByModifiedAtDesc(Pageable pageable); // user 기준 수정일 - @Query("SELECT t FROM Todo t " + - "LEFT JOIN t.user " + - "WHERE t.id = :todoId") - Optional findByIdWithUser(@Param("todoId") Long todoId); //user + + //QueryDSL로 삭제 +// @Query("SELECT t FROM Todo t " + +// "LEFT JOIN t.user " + +// "WHERE t.id = :todoId") +// Optional findByIdWithUser(@Param("todoId") Long todoId); //user // // Todo로 받아서 처리할려고 했는데 TodoResponse로 받아도 상관 없을듯? -> if문도 길어지므로 JPQL문 하나로 써보기 diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java new file mode 100644 index 000000000..7bbb691d2 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java @@ -0,0 +1,35 @@ +package org.example.expert.domain.todo.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +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.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 TodoRepositoryImpl implements CustomTodoRepository { + + private JPAQueryFactory queryFactory; + + @Override + public Optional findByIdWithUser(Long todoId) { + + Todo result = queryFactory + .selectFrom(todo) + .leftJoin(todo.user, user).fetchJoin() + .where(todo.id.eq(todoId)) + .fetchOne(); + + return Optional.ofNullable(result); + + + } + +} 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 c715c9155..77335b93d 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 @@ -82,6 +82,13 @@ public Page getTodos(String weather, LocalDateTime start, LocalDat )); } + + // 이걸 queryDSL로 만들어야함 + // QueryDSL만드는 법 + // 1. 커스텀 레포지토리를 만든다 + // 2. 구현체를 만든다 <- 이거 만들려면 config까지 추가시켜야함. bean 등록해야함. 따로 클래스가 없는거 같으니 하나 만들어주지 뭐,... + // 3. 커스텀 레포지토리를 도메인 레포지토리에 상속시킨다. + public TodoResponse getTodo(long todoId) { Todo todo = todoRepository.findByIdWithUser(todoId) .orElseThrow(() -> new InvalidRequestException("Todo not found")); From f22ba13948099f6a2324b8d011e8054db8018092 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Fri, 3 Apr 2026 13:36:28 +0900 Subject: [PATCH 12/16] =?UTF-8?q?lv9=20:=20SpringSecurity=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++ .../config/AuthUserArgumentResolver.java | 2 + .../example/expert/config/FilterConfig.java | 2 + .../org/example/expert/config/JwtFilter.java | 3 + .../expert/config/JwtSecurityFilter.java | 69 +++++++++++++++++++ .../expert/config/UserDetailsImpl.java | 57 +++++++++++++++ .../org/example/expert/config/WebConfig.java | 1 + .../expert/config/WebSecurityConfig.java | 49 +++++++++++++ .../comment/controller/CommentController.java | 9 ++- .../expert/domain/common/dto/AuthUser.java | 6 +- .../manager/controller/ManagerController.java | 9 ++- .../todo/controller/TodoController.java | 5 +- .../user/controller/UserAdminController.java | 2 + .../user/controller/UserController.java | 5 +- 14 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/example/expert/config/JwtSecurityFilter.java create mode 100644 src/main/java/org/example/expert/config/UserDetailsImpl.java create mode 100644 src/main/java/org/example/expert/config/WebSecurityConfig.java diff --git a/build.gradle b/build.gradle index 68afdeb8b..50cd983a9 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + + + // springSecurity 추가 + implementation 'org.springframework.boot:spring-boot-starter-security' + + 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' diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index e559dfd57..fc85faf9e 100644 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java @@ -12,6 +12,8 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; + +// springSecurity를 쓰기때문에 필요 없을거얌 public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { @Override diff --git a/src/main/java/org/example/expert/config/FilterConfig.java b/src/main/java/org/example/expert/config/FilterConfig.java index 34cb4088a..cf41cb50a 100644 --- a/src/main/java/org/example/expert/config/FilterConfig.java +++ b/src/main/java/org/example/expert/config/FilterConfig.java @@ -9,6 +9,8 @@ @RequiredArgsConstructor public class FilterConfig { + + //WebSecurityConfig로 통합 private final JwtUtil jwtUtil; @Bean diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 03908abe1..e3de23b10 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -14,6 +14,9 @@ import java.io.IOException; + +//WebSecurityConfig로 통합 +//이 클래스는 현시간부로 파기한다 아쎄이! @Slf4j @RequiredArgsConstructor public class JwtFilter implements Filter { diff --git a/src/main/java/org/example/expert/config/JwtSecurityFilter.java b/src/main/java/org/example/expert/config/JwtSecurityFilter.java new file mode 100644 index 000000000..ad07403d9 --- /dev/null +++ b/src/main/java/org/example/expert/config/JwtSecurityFilter.java @@ -0,0 +1,69 @@ +package org.example.expert.config; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + + +//JWT 필터를 다시 만들어보자 +// 중복실행이 되지 않도록 OncePerRequestFilter를 상속받아주구 +// 토큰 뽑아내주구 +// 클레임 데이터 뽑아주구 +// Spring Security 인증 객체 생성하고 저장해주구 +// 필터 체인 유지해주구 +// 컨트롤러 연결해주면 오와리다! +@Slf4j +@RequiredArgsConstructor +public class JwtSecurityFilter extends OncePerRequestFilter { + + //1. jwtUtil 불러와! + private final JwtUtil jwtUtil; + + + //2. 요청, 응답, 필터 + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + //3. 헤더에 오토리제이션을 내놓으시용, JWT 토큰내놔 + String bearerToken = request.getHeader("Authorization"); + + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + String token = jwtUtil.substringToken(bearerToken); + + //4. 유틸로 토큰을 검증하고~ 토큰 만들때 넣었던 4가지 정보를 변수로 설정해~ + try { + Claims claims = jwtUtil.extractClaims(token); + Long userId = Long.parseLong(claims.getSubject()); + String email = claims.get("email", String.class); + UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class)); + String nickname = claims.get("nickname",String.class); // nickname 추가해봤어염 + + // 5. 여기서 작업해둔 SpringSecurity 인증(User) 객체 만들고 ContextHolder에 저장 + UserDetailsImpl userDetails = new UserDetailsImpl(userId, email, userRole, nickname); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (Exception e) { + log.error("JWT 인증 실패", e); + } + } + + + //6. 토큰검사 끝났나염? -> 필터 작동 -> 다음 단계로 넘어가기~ + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/expert/config/UserDetailsImpl.java b/src/main/java/org/example/expert/config/UserDetailsImpl.java new file mode 100644 index 000000000..0f7212766 --- /dev/null +++ b/src/main/java/org/example/expert/config/UserDetailsImpl.java @@ -0,0 +1,57 @@ +package org.example.expert.config; +import lombok.Getter; +import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.core.GrantedAuthority; // 권한 설정을 위해 필요하다구~ +import org.springframework.security.core.authority.SimpleGrantedAuthority; // 권한 설정을 위해 필요하다구~ +import org.springframework.security.core.userdetails.UserDetails; //user를 객체에 담아담아~ + +import java.util.Collection; +import java.util.List; + + +//음 일단 스프링 시큐리티에서 제공하는 UserDetails를 ,contextHolder에 담아야 혀 +//스프링 시큐리티에서 제거하는 UserDetails를 상속받아서 Implements 클래스를 하나 만들자 +// 아이디랑, 이메일이랑, 권한이랑, lv2 연계해서 닉네임이랑 +@Getter +public class UserDetailsImpl implements UserDetails { + + private final Long userId; + private final String email; + private final UserRole userRole; + private final String nickname; + + public UserDetailsImpl(Long userId, String email, UserRole userRole, String nickname) { + this.userId = userId; + this.email = email; + this.userRole = userRole; + this.nickname = nickname; + } + + // 권한을 어떤 형태로든 담아도 됨 -> Collection으로 + // 근데 springSecurity 규격을 맞차야되서 ROLE은 붙여야되~ 불변 리스트로 만들어버리기~ + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_" + userRole.name())); + } + + // 패스워드 넣고 + @Override + public String getPassword() { return null; } + + // 이름 넣고 + @Override + public String getUsername() { return email; } + + // 계정 상태도 췌크 췌크~ 만료됨? 잠김? 사용 가능함? + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isEnabled() { return true; } +} diff --git a/src/main/java/org/example/expert/config/WebConfig.java b/src/main/java/org/example/expert/config/WebConfig.java index adff06b82..6cf2527e6 100644 --- a/src/main/java/org/example/expert/config/WebConfig.java +++ b/src/main/java/org/example/expert/config/WebConfig.java @@ -11,6 +11,7 @@ @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + //WebSecurityConfig로 통합 // ArgumentResolver 등록 @Override public void addArgumentResolvers(List resolvers) { diff --git a/src/main/java/org/example/expert/config/WebSecurityConfig.java b/src/main/java/org/example/expert/config/WebSecurityConfig.java new file mode 100644 index 000000000..07ed96561 --- /dev/null +++ b/src/main/java/org/example/expert/config/WebSecurityConfig.java @@ -0,0 +1,49 @@ +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.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class WebSecurityConfig { + + + //UserDetail 만들었고, JwtSecurityFilter 만들었고 이제 FilterConfig, WebConfig, jwtFilter 다 합쳐버려~ + //필터체인을 만들거임 + + private final JwtUtil jwtUtil; + + // 1. csrf는 잠시 꺼둬~ 우리는 뉴진스 쿠키가 아닌 jwt토큰을 쓸거니께루 + // 2. 토큰만 쓸거라 세션도 쓰지 않을거니 무상태로 둬버리고 + // 3. URL 에다가 접근권한 설정할거임 + // 가. 회원가입은 로그인 누구나 가능 나. admin이 있으니 admin 페이지 갈때 권한 체크 다. 그외는 로그인해야 할 수 있음으로 처리할게염 + // 4. 정수기 맹키로 필터 갈아껴주기~ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + //1. + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + //2. + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 미사용 + //3. - 가,나,다 + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/**").permitAll() // 회원가입, 로그인 허용 + .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 권한 체크 + .anyRequest().authenticated() + ) + //4. + .addFilterBefore(new JwtSecurityFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class) + .build(); + + + // 아 이제 컨트롤러 가서 @AuthenticationPrincipal 어노테이션으로 스프링 시큐리티 방식으로 바꿔야함 + // authUser 수정도 해야함 + } +} 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 1b18c0ece..f5de553e8 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 @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.example.expert.config.UserDetailsImpl; import org.example.expert.domain.comment.dto.request.CommentSaveRequest; import org.example.expert.domain.comment.dto.response.CommentResponse; import org.example.expert.domain.comment.dto.response.CommentSaveResponse; @@ -9,6 +10,7 @@ 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,10 +23,13 @@ public class CommentController { @PostMapping("/todos/{todoId}/comments") public ResponseEntity saveComment( - @Auth AuthUser authUser, + @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable long todoId, @Valid @RequestBody CommentSaveRequest commentSaveRequest - ) { + + ) {// 이거 authUser가 너무 많아서 메서드 인자 타입을 UserDetailsImpl로 바꿔야 겟당 + // 서비스 로직 잘못건드렸다간 망할거 같음요.. + AuthUser authUser = new AuthUser((userDetails.getUserId()), userDetails.getEmail(), userDetails.getUserRole(), userDetails.getNickname()); return ResponseEntity.ok(commentService.saveComment(authUser, todoId, commentSaveRequest)); } 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 7616d1759..9eca772b1 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 @@ -3,18 +3,20 @@ import lombok.Getter; import org.example.expert.domain.user.enums.UserRole; +import java.util.List; + @Getter public class AuthUser { private final Long id; private final String email; - private final UserRole userRole; + private final Collection authorities; private final String nickname; public AuthUser(Long id, String email, UserRole userRole, String nickname ) { this.id = id; this.email = email; - this.userRole = userRole; + this.authorities = List.of(new SimpleGrantedAuthority(userRole.name())); this.nickname = nickname; } } 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..f5a0a5278 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,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.example.expert.config.UserDetailsImpl; 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; @@ -9,6 +10,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,10 +23,11 @@ public class ManagerController { @PostMapping("/todos/{todoId}/managers") public ResponseEntity saveManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable long todoId, @Valid @RequestBody ManagerSaveRequest managerSaveRequest ) { + AuthUser authUser = new AuthUser((userDetails.getUserId()), userDetails.getEmail(), userDetails.getUserRole(), userDetails.getNickname()); return ResponseEntity.ok(managerService.saveManager(authUser, todoId, managerSaveRequest)); } @@ -35,10 +38,10 @@ public ResponseEntity> getMembers(@PathVariable long todoI @DeleteMapping("/todos/{todoId}/managers/{managerId}") public void deleteManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable long todoId, @PathVariable long managerId - ) { + ) { AuthUser authUser = new AuthUser((userDetails.getUserId()), userDetails.getEmail(), userDetails.getUserRole(), userDetails.getNickname()); managerService.deleteManager(authUser, todoId, 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 1fc6484c9..1e44305d9 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,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.example.expert.config.UserDetailsImpl; 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; @@ -10,6 +11,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.*; import java.time.LocalDateTime; @@ -22,9 +24,10 @@ public class TodoController { @PostMapping("/todos") public ResponseEntity saveTodo( - @Auth AuthUser authUser, + @AuthenticationPrincipal UserDetailsImpl userDetails, @Valid @RequestBody TodoSaveRequest todoSaveRequest ) { + AuthUser authUser = new AuthUser((userDetails.getUserId()), userDetails.getEmail(), userDetails.getUserRole(), userDetails.getNickname()); return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest)); } 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..a47ca9d06 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 @@ -14,6 +14,8 @@ public class UserAdminController { private final UserAdminService userAdminService; + + @PatchMapping("/admin/users/{userId}") 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 bb1ef7a95..579c0cc9e 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,14 @@ package org.example.expert.domain.user.controller; import lombok.RequiredArgsConstructor; +import org.example.expert.config.UserDetailsImpl; 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 +23,8 @@ public ResponseEntity getUser(@PathVariable long userId) { } @PutMapping("/users") - public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { + public void changePassword(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { + AuthUser authUser = new AuthUser((userDetails.getUserId()), userDetails.getEmail(), userDetails.getUserRole(), userDetails.getNickname()); userService.changePassword(authUser.getId(), userChangePasswordRequest); } } From 3658316dd3e3e20c582ffd7caa1e5b7abf6c2ed5 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Fri, 3 Apr 2026 17:08:23 +0900 Subject: [PATCH 13/16] =?UTF-8?q?LV10=20:=20QueryDSL=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/QTodoSearchResponse.java | 21 +++++ .../expert/domain/common/dto/AuthUser.java | 3 + .../todo/controller/TodoController.java | 16 ++++ .../todo/dto/request/TodoSearchRequest.java | 20 +++++ .../todo/dto/response/TodoSearchResponse.java | 26 +++++++ .../todo/repository/CustomTodoRepository.java | 13 ++++ .../todo/repository/TodoRepositoryImpl.java | 78 +++++++++++++++++-- .../domain/todo/service/TodoService.java | 9 +++ .../expert/domain/user/entity/User.java | 14 ++-- 9 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 src/main/generated/org/example/expert/domain/todo/dto/response/QTodoSearchResponse.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/request/TodoSearchRequest.java create mode 100644 src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchResponse.java diff --git a/src/main/generated/org/example/expert/domain/todo/dto/response/QTodoSearchResponse.java b/src/main/generated/org/example/expert/domain/todo/dto/response/QTodoSearchResponse.java new file mode 100644 index 000000000..731b68f51 --- /dev/null +++ b/src/main/generated/org/example/expert/domain/todo/dto/response/QTodoSearchResponse.java @@ -0,0 +1,21 @@ +package org.example.expert.domain.todo.dto.response; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.ConstructorExpression; +import javax.annotation.processing.Generated; + +/** + * org.example.expert.domain.todo.dto.response.QTodoSearchResponse is a Querydsl Projection type for TodoSearchResponse + */ +@Generated("com.querydsl.codegen.DefaultProjectionSerializer") +public class QTodoSearchResponse extends ConstructorExpression { + + private static final long serialVersionUID = -1940571367L; + + public QTodoSearchResponse(com.querydsl.core.types.Expression title, com.querydsl.core.types.Expression managerCount, com.querydsl.core.types.Expression commentCount) { + super(TodoSearchResponse.class, new Class[]{String.class, long.class, long.class}, title, managerCount, commentCount); + } + +} + 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 9eca772b1..19a67c112 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,7 +2,10 @@ import lombok.Getter; import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.util.Collection; import java.util.List; @Getter 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 1e44305d9..20ed5fc97 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 @@ -6,10 +6,14 @@ 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.request.TodoSearchRequest; 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.TodoSearchResponse; import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -46,4 +50,16 @@ public ResponseEntity> getTodos( public ResponseEntity getTodo(@PathVariable long todoId) { return ResponseEntity.ok(todoService.getTodo(todoId)); } + + + // API 추가 + @GetMapping("/todos/search") + public ResponseEntity> searchTodos( + @ModelAttribute TodoSearchRequest request, // 쿼리 파라미터를 DTO로 받음 + @PageableDefault(size = 10) Pageable pageable // 페이징 처리 + ) { + return ResponseEntity.ok(todoService.searchTodos(request, pageable)); + } + + } diff --git a/src/main/java/org/example/expert/domain/todo/dto/request/TodoSearchRequest.java b/src/main/java/org/example/expert/domain/todo/dto/request/TodoSearchRequest.java new file mode 100644 index 000000000..9df90b69b --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/request/TodoSearchRequest.java @@ -0,0 +1,20 @@ +package org.example.expert.domain.todo.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + + +//내가 검색할 조건들을 만들어 줘야겠쥬? +@Getter +@Setter +@NoArgsConstructor +public class TodoSearchRequest { + + private String title; + private String nickname; + private LocalDateTime startDate; + private LocalDateTime endDate; +} \ No newline at end of file diff --git a/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchResponse.java b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchResponse.java new file mode 100644 index 000000000..7c1b2da82 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/dto/response/TodoSearchResponse.java @@ -0,0 +1,26 @@ +package org.example.expert.domain.todo.dto.response; + + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class TodoSearchResponse { + + //Entity를 대신할 dto먼저 작성해버리기 + // 반환할 검색 결과 : 제목, 담당자수, 댓글 수 + + private String title; + private Long managerCount; + private Long commentCount; + + @QueryProjection + public TodoSearchResponse(String title, Long managerCount, Long commentCount) { + this.title = title; + this.managerCount = managerCount; + this.commentCount = commentCount; + } + +} diff --git a/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java index 8427b5a93..4aaced765 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java @@ -1,9 +1,22 @@ package org.example.expert.domain.todo.repository; +import org.example.expert.domain.todo.dto.request.TodoSearchRequest; +import org.example.expert.domain.todo.dto.response.TodoSearchResponse; import org.example.expert.domain.todo.entity.Todo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import java.time.LocalDateTime; import java.util.Optional; public interface CustomTodoRepository { Optional findByIdWithUser(Long todoId); + + // 동적 쿼리기반 검색기능을만들어봅시둡 + // 메서드 추가해주구 + // 고려사항 : 제목, 생성일 최신순, 담당자 닉네임, 검색결과는 페이지로 처리해야되니 Pageable도 추가~ + // 검색조건 DTO 만들고 페이지처리 추가 + + Page searchTodos(TodoSearchRequest request, Pageable pageable); + } diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java index 7bbb691d2..52f87067e 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java @@ -1,22 +1,30 @@ package org.example.expert.domain.todo.repository; +import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.todo.entity.QTodo; +import org.example.expert.domain.todo.dto.request.TodoSearchRequest; +import org.example.expert.domain.todo.dto.response.QTodoSearchResponse; +import org.example.expert.domain.todo.dto.response.TodoSearchResponse; 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 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; +import static org.example.expert.domain.manager.entity.QManager.manager; // 추가 +import static org.example.expert.domain.comment.entity.QComment.comment; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; @Repository @RequiredArgsConstructor public class TodoRepositoryImpl implements CustomTodoRepository { - private JPAQueryFactory queryFactory; + private final JPAQueryFactory queryFactory; @Override public Optional findByIdWithUser(Long todoId) { @@ -28,8 +36,66 @@ public Optional findByIdWithUser(Long todoId) { .fetchOne(); return Optional.ofNullable(result); + } + // 이제 검색에 반영할 쿼리문을 만들어야됨~ + // 고려사항 : 제목, 생성일 최신순, 담당자 닉네임, 검색결과는 페이지로 처리해야되니 Pageable도 추가~ + + // 작업순서 + // 0. build했으니 Q클래스는 생략 + // 1. booleanBuilder로 동적쿼리 생성 + // 2. 담을 객체(컨테이너) 생성 + @Override + public Page searchTodos(TodoSearchRequest request, + Pageable pageable){ + + //1. * contains랑 날짜 between으로 설정해주기 + // 흐음 추후 검색조건 바꿀려면 빌더랑 request만 바꿔줍시당 : 시간되면 booleanExpression도 써보기 + BooleanBuilder builder = new BooleanBuilder(); + + if(request.getTitle()!= null) builder.and(todo.title.contains((request.getTitle()))); + if(request.getNickname() != null) builder.and(manager.user.nickname.contains(request.getNickname())); + if(request.getEndDate() != null && request.getStartDate() != null) + builder.and(todo.createdAt.between(request.getStartDate(), request.getEndDate())); + + + //2. content 빌드해주기 -> response에 담을거임 + // 제목, 매니저(id), 댓글(id) 넣어줄건데 + // select, countDistinct, from, leftjoin, where, groupBy, orderBy, offset.limit -> 이거 다 써야함. + // select <- Q클래스 리스폰스에 담아줘야함 근데 중복값은 허용하지 않아(countDistinct) + // from <- 당연히 일정을 기준으로 + // leftjoin <- 일정기준으로 담당자와 댓글을 왼쪽으로 몰거임 + // where <- booleanBuilder에 있는 검색조건으로 조립할거임 + // GroupBy <- 일정 id별로 묶을거임 + // orderBy <- 생성일 기준으로 내림차순 정렬 + // offset().limit() <- 몇번째 페이지부터 몇개 가져올지 (강의 참고) + // fetch <- 결과 내놔 + List content = queryFactory + .select(new QTodoSearchResponse( + todo.title, + manager.id.countDistinct(), + comment.id.countDistinct() + )) + .from(todo) + .leftJoin(todo.managers, manager) + .leftJoin(todo.comments, comment) + .where(builder) + .groupBy(todo.id) + .orderBy(todo.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 3.최종 합계 + + Long total = queryFactory + .select(todo.count()) + .from(todo) + .where(builder) + .fetchOne(); + //4.내용이랑 페이지 가져오는데 null일 경우에 그냥 0으로 처리해버리기 + return new PageImpl<>(content, pageable, total != null ? total : 0L); } } 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 77335b93d..2739f90e1 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,8 +5,10 @@ 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.request.TodoSearchRequest; 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.TodoSearchResponse; 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; @@ -105,4 +107,11 @@ public TodoResponse getTodo(long todoId) { todo.getModifiedAt() ); } + + // 동적 쿼리 검색 메서드 만들기 + public Page searchTodos(TodoSearchRequest request, Pageable pageable) { + return todoRepository.searchTodos(request, pageable); + } + + } 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 cddd9489e..5347a7872 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 @@ -5,7 +5,9 @@ import lombok.NoArgsConstructor; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.common.entity.Timestamped; -import org.example.expert.domain.user.enums.UserRole; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; @Getter @Entity @@ -19,18 +21,18 @@ public class User extends Timestamped { private String email; private String password; @Enumerated(EnumType.STRING) - private UserRole userRole; + private Collection userRole; @Column(unique = true) private String nickname; - public User(String email, String password, UserRole userRole, String nickname) { + public User(String email, String password, Collection userRole, String nickname) { this.email = email; this.password = password; this.userRole = userRole; this.nickname = nickname; } - private User(Long id, String email, UserRole userRole, String nickname) { + private User(Long id, String email, Collection userRole, String nickname) { this.id = id; this.email = email; this.userRole = userRole; @@ -38,14 +40,14 @@ private User(Long id, String email, UserRole userRole, String nickname) { } public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname()); + return new User(authUser.getId(), authUser.getEmail(), authUser.getAuthorities(), authUser.getNickname()); } public void changePassword(String password) { this.password = password; } - public void updateRole(UserRole userRole) { + public void updateRole(Collection userRole) { this.userRole = userRole; } } From a6619f17c3b5c73f8c7cb03db8a61362fde5930c Mon Sep 17 00:00:00 2001 From: cherry pick Date: Fri, 3 Apr 2026 17:32:31 +0900 Subject: [PATCH 14/16] =?UTF-8?q?lv11=20:=20=EB=A1=9C=EA=B7=B8=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/expert/domain/log/Log.java | 35 +++++++++++++++++++ .../expert/domain/log/LogRepository.java | 6 ++++ .../example/expert/domain/log/LogService.java | 24 +++++++++++++ .../manager/service/ManagerService.java | 9 +++++ 4 files changed, 74 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/log/Log.java create mode 100644 src/main/java/org/example/expert/domain/log/LogRepository.java create mode 100644 src/main/java/org/example/expert/domain/log/LogService.java diff --git a/src/main/java/org/example/expert/domain/log/Log.java b/src/main/java/org/example/expert/domain/log/Log.java new file mode 100644 index 000000000..ad75e0a2e --- /dev/null +++ b/src/main/java/org/example/expert/domain/log/Log.java @@ -0,0 +1,35 @@ +package org.example.expert.domain.log; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + + +// 음 로그 테이블을 만들라고 했으니 entity를 작성해주고 +// 작성할거 : id, 로그내용, 생성일 +// 생성자는 메세지만 + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "log") +@EntityListeners(AuditingEntityListener.class) +public class Log { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String message; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + public Log(String message) { + this.message = message; + } +} diff --git a/src/main/java/org/example/expert/domain/log/LogRepository.java b/src/main/java/org/example/expert/domain/log/LogRepository.java new file mode 100644 index 000000000..4d50c3c7a --- /dev/null +++ b/src/main/java/org/example/expert/domain/log/LogRepository.java @@ -0,0 +1,6 @@ +package org.example.expert.domain.log; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LogRepository extends JpaRepository { +} diff --git a/src/main/java/org/example/expert/domain/log/LogService.java b/src/main/java/org/example/expert/domain/log/LogService.java new file mode 100644 index 000000000..c91fefd37 --- /dev/null +++ b/src/main/java/org/example/expert/domain/log/LogService.java @@ -0,0 +1,24 @@ +package org.example.expert.domain.log; + +import lombok.RequiredArgsConstructor; +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; + + + // 흐음 요약하자면 매니저 등록 성공 실패 여부 상관없이 난 로그를 남기겠다는 내용 + // 부모 트랜잭션으로부터 독립한다! + // 독립상태로 log작업은 무조건 db에 저장하겠다 + // 이거 쓸라믄 자기호출 문제때문에 무조건 서로 다른 클래스에서 호출해야함. + @Transactional(propagation = Propagation.REQUIRES_NEW) // 여기가 핵심! + public void saveLog(String message) { + logRepository.save(new Log(message)); + } +} 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..ccd764c92 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 @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.common.exception.InvalidRequestException; +import org.example.expert.domain.log.LogService; 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; @@ -28,9 +29,17 @@ public class ManagerService { private final ManagerRepository managerRepository; private final UserRepository userRepository; private final TodoRepository todoRepository; + private final LogService logService; @Transactional public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) { + // log 기록 생성 +// logService.saveLog("매니저 등록 시도"); + + // 프로젝트 해보니 로그 기록이 중요하니 상세하게 작성~ + logService.saveLog("매니저 등록 시도 - 일정ID: " + todoId + ", 등록대상ID: " + managerSaveRequest.getManagerUserId()); + + // 일정을 만든 유저 User user = User.fromAuthUser(authUser); Todo todo = todoRepository.findById(todoId) From ed877573cf4bae3501c759eaf7a7a5026e10bf11 Mon Sep 17 00:00:00 2001 From: cherry pick Date: Mon, 6 Apr 2026 23:03:33 +0900 Subject: [PATCH 15/16] =?UTF-8?q?lv12=20:=20aws=20=ED=99=9C=EC=9A=A9=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=9C=20=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/expert/domain/HealthCheckController.java | 4 ++++ src/main/java/org/example/expert/domain/S3Config.java | 4 ++++ src/main/java/org/example/expert/domain/S3Controller.java | 4 ++++ src/main/java/org/example/expert/domain/S3Service.java | 4 ++++ 4 files changed, 16 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/HealthCheckController.java create mode 100644 src/main/java/org/example/expert/domain/S3Config.java create mode 100644 src/main/java/org/example/expert/domain/S3Controller.java create mode 100644 src/main/java/org/example/expert/domain/S3Service.java diff --git a/src/main/java/org/example/expert/domain/HealthCheckController.java b/src/main/java/org/example/expert/domain/HealthCheckController.java new file mode 100644 index 000000000..8f8626e64 --- /dev/null +++ b/src/main/java/org/example/expert/domain/HealthCheckController.java @@ -0,0 +1,4 @@ +package org.example.expert.domain; + +public class healthCheckController { +} diff --git a/src/main/java/org/example/expert/domain/S3Config.java b/src/main/java/org/example/expert/domain/S3Config.java new file mode 100644 index 000000000..3a70a672f --- /dev/null +++ b/src/main/java/org/example/expert/domain/S3Config.java @@ -0,0 +1,4 @@ +package org.example.expert.domain; + +public class S3Config { +} diff --git a/src/main/java/org/example/expert/domain/S3Controller.java b/src/main/java/org/example/expert/domain/S3Controller.java new file mode 100644 index 000000000..d0b50e25b --- /dev/null +++ b/src/main/java/org/example/expert/domain/S3Controller.java @@ -0,0 +1,4 @@ +package org.example.expert.domain; + +public class S3Controller { +} diff --git a/src/main/java/org/example/expert/domain/S3Service.java b/src/main/java/org/example/expert/domain/S3Service.java new file mode 100644 index 000000000..83a6f7fb3 --- /dev/null +++ b/src/main/java/org/example/expert/domain/S3Service.java @@ -0,0 +1,4 @@ +package org.example.expert.domain; + +public class S3Service { +} From 1bcb24263f492036170ed82ac68e54c51e58b030 Mon Sep 17 00:00:00 2001 From: Hojin-LIM1 Date: Tue, 7 Apr 2026 11:19:27 +0900 Subject: [PATCH 16/16] Update README.md --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index e19f8e392..a4475953c 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ # SPRING PLUS +# ec2 생성ec2 생성완료 +# db생성db 생성완료 +# ec2 & db 연결서버 연결 +# ec2 구동구동완료 + +# s3 버킷 생성s3 버킷 생성 + + +### 1. 대용량 데이터 삽입 (Bulk Insert) +- **사용 기술**: `JdbcTemplate` (Batch Update) +- **최적화 설정**: JDBC URL 옵션에 `rewriteBatchedStatements=true` 적용 +- **데이터 양**: 5,000,000건 +- **결과**: 총 **88초** 소요 (약 1분 28초) +- **분석**: JPA의 `saveAll` 방식(단건 호출) 대신 JDBC Batch를 활용하여 네트워크 오버헤드를 최소화함으로써 대량의 데이터를 매우 빠르게 삽입할 수 있었습니다. + +--- + +### 2. 조회 성능 테스트 및 인덱스(Index) 최적화 +유저 닉네임을 조건으로 하는 검색 기능에 대해 인덱스 적용 전/후 성능을 비교 측정했습니다. +- **검색 대상**: `User_4999999` (데이터의 최하단부 검색) + +| 구분 | 검색 방식 | 소요 시간 | 비고 | +| :--- | :--- | :--- | :--- | +| **인덱스 적용 전** | **Full Table Scan** | **4,281ms** | 전체 행(500만건)을 하나씩 대조 | +| **인덱스 적용 후** | **Index Range Scan** | **210ms** | B-Tree 구조를 통해 즉시 접근 (약 20배 향상) |