diff --git a/README.md b/README.md
index e19f8e392..a4475953c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,26 @@
# SPRING PLUS
+# ec2 생성
+# db생성
+# ec2 & db 연결
+# ec2 구동
+
+# 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배 향상) |
diff --git a/build.gradle b/build.gradle
index a7fd3e706..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'
@@ -41,6 +47,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 extends Comment> 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 extends Comment> 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 extends Timestamped> 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 extends Manager> 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 extends Manager> 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/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/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 extends Todo> 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 extends Todo> 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 extends User> path) {
+ super(path.getType(), path.getMetadata());
+ }
+
+ public QUser(PathMetadata metadata) {
+ super(User.class, metadata);
+ }
+
+}
+
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();
diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java
index db00211de..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
@@ -36,11 +38,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/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/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/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/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 extends GrantedAuthority> 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/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 {
+}
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/comment/controller/CommentController.java b/src/main/java/org/example/expert/domain/comment/controller/CommentController.java
index 51264b12e..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,13 +23,19 @@ 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));
}
+ // 로그 분석 : 댓글조회시 마다 사용자 정보를 달라고함 ㄷㄷ
+ // 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/java/org/example/expert/domain/common/dto/AuthUser.java b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java
index 7f4bc52e1..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,17 +2,24 @@
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
public class AuthUser {
private final Long id;
private final String email;
- private final UserRole userRole;
+ private final Collection extends GrantedAuthority> authorities;
+ 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.authorities = List.of(new SimpleGrantedAuthority(userRole.name()));
+ this.nickname = nickname;
}
}
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/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/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/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)
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..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
@@ -2,16 +2,24 @@
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;
+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.*;
+import java.time.LocalDateTime;
+
@RestController
@RequiredArgsConstructor
public class TodoController {
@@ -20,22 +28,38 @@ 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));
}
@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}")
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/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) {
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..4aaced765
--- /dev/null
+++ b/src/main/java/org/example/expert/domain/todo/repository/CustomTodoRepository.java
@@ -0,0 +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/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java
index a3e4e0749..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
@@ -7,15 +7,52 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-import java.util.Optional;
+import java.time.LocalDateTime;
-public interface TodoRepository extends JpaRepository {
+//커스텀 레포 상속시켜주기
+public interface TodoRepository extends JpaRepository, CustomTodoRepository {
+
+ //
+ @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로 쿄쿄 -> if문도 길어지므로 JPQL문 하나로 써보기
+// @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
+// Page findAllByOrderByModifiedAtDesc(Pageable pageable); // 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문 하나로 써보기
+// @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);
- @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);
}
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..52f87067e
--- /dev/null
+++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryImpl.java
@@ -0,0 +1,101 @@
+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.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.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+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 final 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);
+ }
+
+ // 이제 검색에 반영할 쿼리문을 만들어야됨~
+ // 고려사항 : 제목, 생성일 최신순, 담당자 닉네임, 검색결과는 페이지로 처리해야되니 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 922991ce7..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;
@@ -17,14 +19,18 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDateTime;
+
@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);
@@ -47,10 +53,25 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ
);
}
- public Page getTodos(int page, int size) {
+ // 1. 파라미터랑 조건문 추가해주기(일단 내가 많이했던거 써보기~)
+ // 2, 단일 JPQL 만들기
+ 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);
+ // 2, 단일 JPQL 만들기
+ Page todos = todoRepository.findByCondition(weather, start, end, pageable);
+
+
+ // 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(),
@@ -63,6 +84,13 @@ public Page getTodos(int page, int size) {
));
}
+
+ // 이걸 queryDSL로 만들어야함
+ // QueryDSL만드는 법
+ // 1. 커스텀 레포지토리를 만든다
+ // 2. 구현체를 만든다 <- 이거 만들려면 config까지 추가시켜야함. bean 등록해야함. 따로 클래스가 없는거 같으니 하나 만들어주지 뭐,...
+ // 3. 커스텀 레포지토리를 도메인 레포지토리에 상속시킨다.
+
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
@@ -79,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/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);
}
}
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..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,29 +21,33 @@ public class User extends Timestamped {
private String email;
private String password;
@Enumerated(EnumType.STRING)
- private UserRole userRole;
+ private Collection extends GrantedAuthority> userRole;
+ @Column(unique = true)
+ private String nickname;
- public User(String email, String password, UserRole userRole) {
+ public User(String email, String password, Collection extends GrantedAuthority> 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, Collection extends GrantedAuthority> 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.getAuthorities(), authUser.getNickname());
}
public void changePassword(String password) {
this.password = password;
}
- public void updateRole(UserRole userRole) {
+ public void updateRole(Collection extends GrantedAuthority> userRole) {
this.userRole = userRole;
}
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 000000000..2c5dda8c7
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,16 @@
+jwt:
+ secret:
+ 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
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..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
@@ -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(
@@ -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"));
}
}