From ee4fc508e01fbb0a80c54c78396f3067a6204893 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 00:58:45 +0900 Subject: [PATCH 001/478] =?UTF-8?q?chore:=20Issue,=20Pull=20Request=20Temp?= =?UTF-8?q?late=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/issue_template.md | 20 ++++++++++++++ .github/pull_request_template.md | 35 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/issue_template.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 000000000..1172ee039 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,20 @@ +--- +name: "\bIssue 생성 템플릿" +about: 해당 Issue 생성 템플릿을 통하여 Issue를 생성해주세요. +title: 'Feat: Issue 제목' +labels: '' +assignees: '' + +--- + +### 📝 Description + +- 구현할 내용 1 +- 구현할 내용 2 + +--- + +### 📝 Todo + +- [ ] 구현할 내용 1 +- [ ] 구현할 내용 2 \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..e509b97b3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +## ✅ PR 유형 +어떤 변경 사항이 있었나요? + +- [ ] 새로운 기능 추가 +- [ ] 버그 수정 +- [ ] 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경) +- [ ] 코드 리팩토링 +- [ ] 주석 추가 및 수정 +- [ ] 문서 수정 +- [ ] 빌드 부분 혹은 패키지 매니저 수정 +- [ ] 파일 혹은 폴더명 수정 +- [ ] 파일 혹은 폴더 삭제 + +--- + +## 📝 작업 내용 +이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +- 작업한 내용 1 +- 작업한 내용 2 + +--- + +## ✏️ 관련 이슈 +본인이 작업한 내용이 어떤 Issue Number와 관련이 있는지만 작성해주세요 + +ex) +- Fixes : #00 (수정중인 이슈) +- Resolves : #100 (무슨 이슈를 해결했는지) +- Ref : #00 #01 (참고할 이슈) +- Related to : #00 #01 (해당 커밋과 관려) + +--- + +## 🎸 기타 사항 or 추가 코멘트 \ No newline at end of file From 623a4740411d936d964c26a902936b82ddc6c13a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 01:21:52 +0900 Subject: [PATCH 002/478] =?UTF-8?q?chore:=20(#1)=20gradle=EC=97=90=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 5a629f1f7..cba01aa69 100644 --- a/build.gradle +++ b/build.gradle @@ -24,14 +24,34 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' + // WEB implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - annotationProcessor 'org.projectlombok:lombok' + + // Testing testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Development tools (for local development only) + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // DB, JPA + runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // Spring Security & OAuth2 + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // Lombok (code simplification) + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // Validation (for request/response validation) + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // WebSocket (for real-time communication) + implementation 'org.springframework.boot:spring-boot-starter-websocket' } tasks.named('test') { From 88432addf8a97c615d8d137903fd93859abc119c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 01:42:10 +0900 Subject: [PATCH 003/478] =?UTF-8?q?chore:=20(#1)=20gitignore=EC=97=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=ED=8C=8C=EC=9D=BC=EC=9D=84?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ src/main/resources/application.properties | 1 - src/main/resources/application.yml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/.gitignore b/.gitignore index c2065bc26..5b895ba31 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### Configuration ### +src/main/resources/application-*.yml diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 3ca17a4e3..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=backend diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..a5c1568db --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + application: + name: backend From 611d5df0e53665c3c463d5ccbd6d5533a8229b87 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 06:04:44 +0900 Subject: [PATCH 004/478] =?UTF-8?q?feat:=20(#3)=20=EC=84=B1=EA=B3=B5,=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8(=EC=97=90=EB=9F=AC)=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=98=20=EA=B3=B5=ED=86=B5=20=ED=98=95=EC=8B=9D=EC=9D=84=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presentation/ApiResponse.java | 34 +++++++++++++++++++ .../core/presentation/ErrorResponse.java | 18 ++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/spring/backend/core/presentation/ApiResponse.java create mode 100644 src/main/java/spring/backend/core/presentation/ErrorResponse.java diff --git a/src/main/java/spring/backend/core/presentation/ApiResponse.java b/src/main/java/spring/backend/core/presentation/ApiResponse.java new file mode 100644 index 000000000..b5d9252d4 --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/ApiResponse.java @@ -0,0 +1,34 @@ +package spring.backend.core.presentation; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class ApiResponse { + + private boolean success; + + private int status; + + private T body; + + @Builder.Default + private LocalDateTime timestamp = LocalDateTime.now(); + + public static ApiResponse of(boolean success, int status, Object body) { + return ApiResponse.builder() + .success(success) + .status(status) + .body(body) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/presentation/ErrorResponse.java b/src/main/java/spring/backend/core/presentation/ErrorResponse.java new file mode 100644 index 000000000..81ca80408 --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/ErrorResponse.java @@ -0,0 +1,18 @@ +package spring.backend.core.presentation; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + + private final String message; + + private final HttpStatus httpStatus; + + public static ErrorResponse of(String message, HttpStatus httpStatus) { + return new ErrorResponse(message, httpStatus); + } +} From d9cb129043317c33e20c8c18d7d1150a8580940e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 06:06:26 +0900 Subject: [PATCH 005/478] =?UTF-8?q?feat:=20(#3)=20RestControllerAdvice?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=9D=91=EB=8B=B5,=20=EC=98=88=EC=99=B8=EB=A5=BC?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 30 ++++++++++++++ .../core/presentation/ResponseWrapper.java | 39 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/spring/backend/core/presentation/ResponseWrapper.java diff --git a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..e6e6f439c --- /dev/null +++ b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java @@ -0,0 +1,30 @@ +package spring.backend.core.exception; + +import java.util.Optional; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import spring.backend.core.presentation.ErrorResponse; + +@RestControllerAdvice +@Log4j2 +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public final ErrorResponse handleAllExceptions(Exception ex, WebRequest request) { + logger.error("ERROR ::: [AllException] ", ex); + return ErrorResponse.of(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(DomainException.class) + public final ErrorResponse handleDomainException(DomainException ex) { + HttpStatus httpStatus = Optional.ofNullable(ex.getHttpStatus()).orElse(HttpStatus.INTERNAL_SERVER_ERROR); + log.error("ERROR ::: [DomainException] ", ex); + return ErrorResponse.of(ex.getMessage(), httpStatus); + } +} diff --git a/src/main/java/spring/backend/core/presentation/ResponseWrapper.java b/src/main/java/spring/backend/core/presentation/ResponseWrapper.java new file mode 100644 index 000000000..9fb0d0d7a --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/ResponseWrapper.java @@ -0,0 +1,39 @@ +package spring.backend.core.presentation; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.log4j.Log4j2; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +@RestControllerAdvice(basePackages = "spring.backend") +@Log4j2 +public class ResponseWrapper implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + log.info("execute AOP - supports"); + log.info("execute AOP - returnType :: {}", returnType); + log.info("execute AOP - converterType :: {}", converterType); + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + log.info("execute AOP - beforeBodyWrite"); + HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse(); + int status = servletResponse.getStatus(); + if (body instanceof ErrorResponse) { + HttpStatus errorStatus = ((ErrorResponse) body).getHttpStatus(); + return ApiResponse.of(false, errorStatus.value(), body); + } + return ApiResponse.of(true, status, body); + } +} From c362cbbbd4020357aa1b71eebc6f8215c8196a8d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 9 Oct 2024 06:07:36 +0900 Subject: [PATCH 006/478] =?UTF-8?q?feat:=20(#3)=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=EC=99=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=98=88=EC=99=B8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/DomainException.java | 28 +++++++++++++++++++ .../core/exception/error/BaseErrorCode.java | 14 ++++++++++ .../core/exception/error/GlobalErrorCode.java | 22 +++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/main/java/spring/backend/core/exception/DomainException.java create mode 100644 src/main/java/spring/backend/core/exception/error/BaseErrorCode.java create mode 100644 src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java diff --git a/src/main/java/spring/backend/core/exception/DomainException.java b/src/main/java/spring/backend/core/exception/DomainException.java new file mode 100644 index 000000000..a1689ec0b --- /dev/null +++ b/src/main/java/spring/backend/core/exception/DomainException.java @@ -0,0 +1,28 @@ +package spring.backend.core.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +public class DomainException extends RuntimeException { + + private HttpStatus httpStatus; + + private String code; + + public DomainException(String message) { + super(message); + } + + public DomainException(HttpStatus httpStatus, String message) { + super(message); + this.httpStatus = httpStatus; + } + + public DomainException(HttpStatus httpStatus, BaseErrorCode errorCode) { + super(errorCode.getMessage()); + this.httpStatus = httpStatus; + this.code = errorCode.name(); + } +} diff --git a/src/main/java/spring/backend/core/exception/error/BaseErrorCode.java b/src/main/java/spring/backend/core/exception/error/BaseErrorCode.java new file mode 100644 index 000000000..1b6663194 --- /dev/null +++ b/src/main/java/spring/backend/core/exception/error/BaseErrorCode.java @@ -0,0 +1,14 @@ +package spring.backend.core.exception.error; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + String name(); + + String getMessage(); + + HttpStatus getHttpStatus(); + + T toException(); +} diff --git a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java new file mode 100644 index 000000000..c19fceb91 --- /dev/null +++ b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java @@ -0,0 +1,22 @@ +package spring.backend.core.exception.error; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum GlobalErrorCode implements BaseErrorCode { + + ALREADY_PROCESS_STARTED(HttpStatus.BAD_REQUEST, "이미 처리중인 요청입니다."), + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 내부 오류입니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public RuntimeException toException() { + return new RuntimeException(message); + } +} From f1651d718aa164fdd952180652ff5a7d30dec789 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 9 Oct 2024 16:02:33 +0900 Subject: [PATCH 007/478] =?UTF-8?q?chore:=20(#5)=20Dokerfile=EC=9D=84=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 17 +++++++++++++++++ docker-compose.yml | 0 2 files changed, 17 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c259f94cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM gradle:8.10.1-jdk17 AS build + +WORKDIR /app + +COPY . /app + +RUN gradle clean build --no-daemon + +FROM openjdk:17.0.1-jdk-slim + +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar /app/backend.jar + +EXPOSE 8080 +ENTRYPOINT ["java"] +CMD ["-jar", "backend.jar"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..e69de29bb From e778ff8a6c20e4fcf4b9d4d9bcb332b321791a34 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 9 Oct 2024 16:03:42 +0900 Subject: [PATCH 008/478] =?UTF-8?q?chore:=20(#5)=20doker-compose.yml=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e69de29bb..de714c549 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + cnergy-backend: + build: + context: . + dockerfile: Dockerfile + ports: + - '8080:8080' From 60140215bb0a46771627f0819d9f8b4b966b127a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 9 Oct 2024 16:18:46 +0900 Subject: [PATCH 009/478] =?UTF-8?q?chore:=20(#5)=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=8B=9C=20Test=EC=99=80=20docker=20=EB=AA=A8=EB=91=90=20=20ap?= =?UTF-8?q?plication-local.yml=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 ++ src/test/java/spring/backend/BackendApplicationTests.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index de714c549..5b438c12a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,5 @@ services: dockerfile: Dockerfile ports: - '8080:8080' + environment: + SPRING_PROFILES_ACTIVE: local diff --git a/src/test/java/spring/backend/BackendApplicationTests.java b/src/test/java/spring/backend/BackendApplicationTests.java index 6079af5b6..841f0d44d 100644 --- a/src/test/java/spring/backend/BackendApplicationTests.java +++ b/src/test/java/spring/backend/BackendApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("local") class BackendApplicationTests { @Test From 5c8d9e30a76b443a21ed4d20c521c5617f47ff31 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 11 Oct 2024 03:39:35 +0900 Subject: [PATCH 010/478] =?UTF-8?q?fix:=20(#11)=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=EC=99=80=20ResponseWrap?= =?UTF-8?q?per=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presentation/ApiResponse.java | 34 ---------------- .../core/presentation/ResponseWrapper.java | 39 ------------------- 2 files changed, 73 deletions(-) delete mode 100644 src/main/java/spring/backend/core/presentation/ApiResponse.java delete mode 100644 src/main/java/spring/backend/core/presentation/ResponseWrapper.java diff --git a/src/main/java/spring/backend/core/presentation/ApiResponse.java b/src/main/java/spring/backend/core/presentation/ApiResponse.java deleted file mode 100644 index b5d9252d4..000000000 --- a/src/main/java/spring/backend/core/presentation/ApiResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package spring.backend.core.presentation; - -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; - -@Getter -@ToString -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -public class ApiResponse { - - private boolean success; - - private int status; - - private T body; - - @Builder.Default - private LocalDateTime timestamp = LocalDateTime.now(); - - public static ApiResponse of(boolean success, int status, Object body) { - return ApiResponse.builder() - .success(success) - .status(status) - .body(body) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/presentation/ResponseWrapper.java b/src/main/java/spring/backend/core/presentation/ResponseWrapper.java deleted file mode 100644 index 9fb0d0d7a..000000000 --- a/src/main/java/spring/backend/core/presentation/ResponseWrapper.java +++ /dev/null @@ -1,39 +0,0 @@ -package spring.backend.core.presentation; - -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.log4j.Log4j2; -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -@RestControllerAdvice(basePackages = "spring.backend") -@Log4j2 -public class ResponseWrapper implements ResponseBodyAdvice { - - @Override - public boolean supports(MethodParameter returnType, Class> converterType) { - log.info("execute AOP - supports"); - log.info("execute AOP - returnType :: {}", returnType); - log.info("execute AOP - converterType :: {}", converterType); - return true; - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - log.info("execute AOP - beforeBodyWrite"); - HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse(); - int status = servletResponse.getStatus(); - if (body instanceof ErrorResponse) { - HttpStatus errorStatus = ((ErrorResponse) body).getHttpStatus(); - return ApiResponse.of(false, errorStatus.value(), body); - } - return ApiResponse.of(true, status, body); - } -} From 7ded29cfe4e53e4abb88341f395acd2ef1042498 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 11 Oct 2024 05:01:27 +0900 Subject: [PATCH 011/478] =?UTF-8?q?feat:=20(#11)=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=98=20=EC=B6=94=EC=83=81=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EC=99=80=20=EC=83=81=EC=86=8D=EB=B0=9B=EB=8A=94=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/presentation/BaseResponse.java | 15 +++++++++++++++ .../backend/core/presentation/RestResponse.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/main/java/spring/backend/core/presentation/BaseResponse.java create mode 100644 src/main/java/spring/backend/core/presentation/RestResponse.java diff --git a/src/main/java/spring/backend/core/presentation/BaseResponse.java b/src/main/java/spring/backend/core/presentation/BaseResponse.java new file mode 100644 index 000000000..8ccb569a9 --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/BaseResponse.java @@ -0,0 +1,15 @@ +package spring.backend.core.presentation; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class BaseResponse { + + private final boolean success; + + private final LocalDateTime timestamp; +} diff --git a/src/main/java/spring/backend/core/presentation/RestResponse.java b/src/main/java/spring/backend/core/presentation/RestResponse.java new file mode 100644 index 000000000..e5cf97b80 --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/RestResponse.java @@ -0,0 +1,15 @@ +package spring.backend.core.presentation; + +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class RestResponse extends BaseResponse { + + private final T data; + + public RestResponse(T data) { + super(true, LocalDateTime.now()); + this.data = data; + } +} From d148c168707c1bf61d429fe56da2091a5ed7d02e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 11 Oct 2024 05:02:42 +0900 Subject: [PATCH 012/478] =?UTF-8?q?fix:=20(#11)=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20ResponseEntity=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 15 +++++----- .../core/presentation/ErrorResponse.java | 28 ++++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java index e6e6f439c..ee1b80237 100644 --- a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java +++ b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java @@ -3,8 +3,8 @@ import java.util.Optional; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -15,16 +15,17 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public final ErrorResponse handleAllExceptions(Exception ex, WebRequest request) { - logger.error("ERROR ::: [AllException] ", ex); - return ErrorResponse.of(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { + log.error("ERROR ::: [AllException] ", ex); + ErrorResponse errorResponse = ErrorResponse.createErrorResponse().statusCode(500).exception(ex).build(); + return ResponseEntity.internalServerError().body(errorResponse); } @ExceptionHandler(DomainException.class) - public final ErrorResponse handleDomainException(DomainException ex) { + public final ResponseEntity handleDomainException(DomainException ex) { HttpStatus httpStatus = Optional.ofNullable(ex.getHttpStatus()).orElse(HttpStatus.INTERNAL_SERVER_ERROR); log.error("ERROR ::: [DomainException] ", ex); - return ErrorResponse.of(ex.getMessage(), httpStatus); + ErrorResponse errorResponse = ErrorResponse.createDomainErrorResponse().statusCode(httpStatus.value()).exception(ex).build(); + return ResponseEntity.status(httpStatus).body(errorResponse); } } diff --git a/src/main/java/spring/backend/core/presentation/ErrorResponse.java b/src/main/java/spring/backend/core/presentation/ErrorResponse.java index 81ca80408..da063f439 100644 --- a/src/main/java/spring/backend/core/presentation/ErrorResponse.java +++ b/src/main/java/spring/backend/core/presentation/ErrorResponse.java @@ -1,18 +1,32 @@ package spring.backend.core.presentation; +import java.time.LocalDateTime; +import lombok.Builder; import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; @Getter -@RequiredArgsConstructor -public class ErrorResponse { +public class ErrorResponse extends BaseResponse { + + private final int statusCode; + + private final String code; private final String message; - private final HttpStatus httpStatus; + @Builder(builderClassName = "CreateErrorResponse", builderMethodName = "createErrorResponse") + public ErrorResponse(int statusCode, Exception exception) { + super(false, LocalDateTime.now()); + this.statusCode = statusCode; + this.code = exception.getClass().getSimpleName(); + this.message = exception.getMessage(); + } - public static ErrorResponse of(String message, HttpStatus httpStatus) { - return new ErrorResponse(message, httpStatus); + @Builder(builderClassName = "CreateDomainErrorResponse", builderMethodName = "createDomainErrorResponse") + public ErrorResponse(int statusCode, DomainException exception) { + super(false, LocalDateTime.now()); + this.statusCode = statusCode; + this.code = exception.getCode(); + this.message = exception.getMessage(); } } From ebe5790fbb6cb46c13588a9b45db7fa675cf23b7 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 10 Oct 2024 10:24:20 +0900 Subject: [PATCH 013/478] =?UTF-8?q?chore:=20(#7)=20Swagger=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index cba01aa69..e1f166d47 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,9 @@ dependencies { // WebSocket (for real-time communication) implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // Swagger-UI + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' } tasks.named('test') { From 63a2110f8ad69cce659b0f090d14ce003f22171c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 10 Oct 2024 10:28:13 +0900 Subject: [PATCH 014/478] =?UTF-8?q?feat:=20(#7)=20Swagger=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/swagger/ApiErrorCode.java | 14 +++ .../configuration/swagger/ExampleHolder.java | 16 +++ .../swagger/SwaggerConfiguration.java | 102 ++++++++++++++++++ .../core/presentation/ErrorResponse.java | 9 ++ 4 files changed, 141 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/swagger/ApiErrorCode.java create mode 100644 src/main/java/spring/backend/core/configuration/swagger/ExampleHolder.java create mode 100644 src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/swagger/ApiErrorCode.java b/src/main/java/spring/backend/core/configuration/swagger/ApiErrorCode.java new file mode 100644 index 000000000..d34b2faab --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/swagger/ApiErrorCode.java @@ -0,0 +1,14 @@ +package spring.backend.core.configuration.swagger; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import spring.backend.core.exception.error.BaseErrorCode; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiErrorCode { + + Class[] value(); +} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/configuration/swagger/ExampleHolder.java b/src/main/java/spring/backend/core/configuration/swagger/ExampleHolder.java new file mode 100644 index 000000000..78d454c67 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/swagger/ExampleHolder.java @@ -0,0 +1,16 @@ +package spring.backend.core.configuration.swagger; + +import io.swagger.v3.oas.models.examples.Example; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ExampleHolder { + + private Example holder; + + private int code; + + private String name; +} diff --git a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java new file mode 100644 index 000000000..b093bfcab --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java @@ -0,0 +1,102 @@ +package spring.backend.core.configuration.swagger; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.security.SecurityScheme.In; +import io.swagger.v3.oas.models.security.SecurityScheme.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; +import spring.backend.core.exception.error.BaseErrorCode; +import spring.backend.core.presentation.ErrorResponse; + +@Configuration +@OpenAPIDefinition(info = @Info(title = "C-nergy API", description = "C-nergy : API 명세서", version = "v1.0.0")) +public class SwaggerConfiguration { + + @Bean + public OpenAPI openAPI(){ + SecurityScheme securityScheme = new SecurityScheme() + .type(Type.HTTP).scheme("bearer").bearerFormat("JWT") + .in(In.HEADER).name("Authorization"); + SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth"); + + return new OpenAPI() + .components(new Components().addSecuritySchemes("bearerAuth", securityScheme)) + .security(Arrays.asList(securityRequirement)); + } + + @Bean + public OperationCustomizer operationCustomizer() { + return (Operation operation, HandlerMethod handlerMethod) -> { + ApiErrorCode apiErrorCode = handlerMethod.getMethodAnnotation(ApiErrorCode.class); + if (apiErrorCode != null) { + generateErrorCodeResponseExample(operation, apiErrorCode.value()); + } + return operation; + }; + } + + private void generateErrorCodeResponseExample(Operation operation, Class[] types) { + ApiResponses responses = operation.getResponses(); + List exampleHolders = new ArrayList<>(); + + for (Class type : types) { + BaseErrorCode[] errorCodes = type.getEnumConstants(); + Arrays.stream(errorCodes).map( + baseErrorCode -> ExampleHolder.builder() + .holder(getSwaggerExample(baseErrorCode)) + .code(baseErrorCode.getHttpStatus().value()) + .name(baseErrorCode.name()) + .build() + ).forEach(exampleHolders::add); + } + + Map> statusWithExampleHolders = new HashMap<>( + exampleHolders.stream() + .collect(Collectors.groupingBy(ExampleHolder::getCode))); + + addExamplesToResponses(responses, statusWithExampleHolders); + } + + private Example getSwaggerExample(BaseErrorCode baseErrorCode) { + ErrorResponse errorResponse = ErrorResponse.createSwaggerErrorResponse() + .baseErrorCode(baseErrorCode) + .build(); + Example example = new Example(); + example.setValue(errorResponse); + return example; + } + + private void addExamplesToResponses(ApiResponses responses, Map> statusWithExampleHolders) { + statusWithExampleHolders.forEach( + (status, value) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse apiResponse = new ApiResponse(); + value.forEach(exampleHolder -> mediaType.addExamples(exampleHolder.getName(), + exampleHolder.getHolder())); + content.addMediaType("application/json", mediaType); + apiResponse.setContent(content); + responses.addApiResponse(status.toString(), apiResponse); + } + ); + } +} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/presentation/ErrorResponse.java b/src/main/java/spring/backend/core/presentation/ErrorResponse.java index da063f439..d8065e20e 100644 --- a/src/main/java/spring/backend/core/presentation/ErrorResponse.java +++ b/src/main/java/spring/backend/core/presentation/ErrorResponse.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Getter; import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; @Getter public class ErrorResponse extends BaseResponse { @@ -29,4 +30,12 @@ public ErrorResponse(int statusCode, DomainException exception) { this.code = exception.getCode(); this.message = exception.getMessage(); } + + @Builder(builderClassName = "CreateSwaggerErrorResponse", builderMethodName = "createSwaggerErrorResponse") + public ErrorResponse(BaseErrorCode baseErrorCode) { + super(false, LocalDateTime.now()); + this.statusCode = baseErrorCode.getHttpStatus().value(); + this.code = baseErrorCode.name(); + this.message = baseErrorCode.getMessage(); + } } From 17af6e1bca08a470ae9e27e30e46c3614d7bde8b Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 03:48:55 +0900 Subject: [PATCH 015/478] =?UTF-8?q?feat:=20(#13)=20=EC=9D=B8=EC=A6=9D,=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=99=80=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/AuthenticationErrorCode.java | 32 +++++++++++++++++++ .../exception/AuthenticationException.java | 8 +++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java create mode 100644 src/main/java/spring/backend/core/exception/AuthenticationException.java diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java new file mode 100644 index 000000000..534bb3ab2 --- /dev/null +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -0,0 +1,32 @@ +package spring.backend.auth.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.AuthenticationException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum AuthenticationErrorCode implements BaseErrorCode { + + NOT_EXIST_HEADER(HttpStatus.UNAUTHORIZED, "Authorization Header가 존재하지 않습니다."), + NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "Authorization Header에 Token이 존재하지 않습니다."), + NOT_MATCH_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "토큰의 형식이 맞지 않습니다."), + NOT_DEFINE_TOKEN(HttpStatus.UNAUTHORIZED, "정의되지 않은 토큰입니다."), + INVALID_PROVIDER(HttpStatus.BAD_REQUEST, "유효한 OAuth 써드파티 제공자가 아닙니다."), + NOT_EXIST_PROVIDER(HttpStatus.BAD_REQUEST, "OAuth 써드파티 제공자가 존재하지 않습니다."), + NOT_EXIST_AUTH_CODE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 제공받은 인증 코드가 존재하지 않습니다."), + ACCESS_TOKEN_NOT_ISSUED(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 액세스 토큰이 발급되지 않았습니다."), + NOT_EXIST_RESOURCE_RESPONSE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 리소스 서버에서 자원이 존재하지 않습니다."), + RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public AuthenticationException toException() { + return new AuthenticationException(message); + } +} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/exception/AuthenticationException.java b/src/main/java/spring/backend/core/exception/AuthenticationException.java new file mode 100644 index 000000000..f4afc904d --- /dev/null +++ b/src/main/java/spring/backend/core/exception/AuthenticationException.java @@ -0,0 +1,8 @@ +package spring.backend.core.exception; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException(String message) { + super(message); + } +} From 77c3be8aa3f10df157cd1a6fbac6e7c18829b09f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 03:50:40 +0900 Subject: [PATCH 016/478] =?UTF-8?q?feat:=20(#13)=20OAuth=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EA=B0=92=EC=9D=84=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=EB=B0=9B=EB=8A=94=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../property/GoogleOAuthProperty.java | 14 +++++++++++ .../property/KakaoOAuthProperty.java | 14 +++++++++++ .../property/NaverOAuthProperty.java | 14 +++++++++++ .../property/shared/BaseOAuthProperty.java | 25 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java create mode 100644 src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java create mode 100644 src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java create mode 100644 src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java diff --git a/src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java new file mode 100644 index 000000000..58f3932e0 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java @@ -0,0 +1,14 @@ +package spring.backend.core.configuration.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import spring.backend.core.configuration.property.shared.BaseOAuthProperty; + +@Component +@Getter +@Setter +@ConfigurationProperties("oauth2.google") +public class GoogleOAuthProperty extends BaseOAuthProperty { +} diff --git a/src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java new file mode 100644 index 000000000..5d61d853b --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java @@ -0,0 +1,14 @@ +package spring.backend.core.configuration.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import spring.backend.core.configuration.property.shared.BaseOAuthProperty; + +@Component +@Getter +@Setter +@ConfigurationProperties("oauth2.kakao") +public class KakaoOAuthProperty extends BaseOAuthProperty { +} diff --git a/src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java new file mode 100644 index 000000000..8fcc32be1 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java @@ -0,0 +1,14 @@ +package spring.backend.core.configuration.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import spring.backend.core.configuration.property.shared.BaseOAuthProperty; + +@Component +@Getter +@Setter +@ConfigurationProperties("oauth2.naver") +public class NaverOAuthProperty extends BaseOAuthProperty { +} diff --git a/src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java new file mode 100644 index 000000000..f272f2572 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java @@ -0,0 +1,25 @@ +package spring.backend.core.configuration.property.shared; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +public class BaseOAuthProperty { + + protected String clientId; + + protected String clientSecret; + + protected String redirectUri; + + protected Set scope; + + protected String tokenUri; + + protected String resourceUri; + + protected String authUri; +} From 7d667b5da3f2c942f7b3e41aa74877474c48eb7e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 03:54:09 +0900 Subject: [PATCH 017/478] =?UTF-8?q?feat:=20(#13)=20OAuthRestClient?= =?UTF-8?q?=EC=99=80=20provider=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8A=94=20=ED=8C=A9=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/OAuthRestClient.java | 15 ++++++++ .../OAuthRestClientFactory.java | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/OAuthRestClientFactory.java diff --git a/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java new file mode 100644 index 000000000..9e7aa3b92 --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java @@ -0,0 +1,15 @@ +package spring.backend.auth.infrastructure; + +import spring.backend.auth.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.dto.response.OAuthResourceResponse; + +import java.net.URI; + +public interface OAuthRestClient { + + URI getAuthUrl(); + + OAuthAccessTokenResponse getAccessToken(String authCode, String state); + + OAuthResourceResponse getResource(String oauthToken); +} diff --git a/src/main/java/spring/backend/auth/infrastructure/OAuthRestClientFactory.java b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClientFactory.java new file mode 100644 index 000000000..4ae00826b --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClientFactory.java @@ -0,0 +1,35 @@ +package spring.backend.auth.infrastructure; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.google.GoogleOAuthRestClient; +import spring.backend.auth.infrastructure.kakao.KakaoOAuthRestClient; +import spring.backend.auth.infrastructure.naver.NaverOAuthRestClient; +import spring.backend.member.domain.value.Provider; + +@Component +@RequiredArgsConstructor +public class OAuthRestClientFactory { + + private final GoogleOAuthRestClient googleOAuthRestClient; + + private final NaverOAuthRestClient naverOAuthRestClient; + + private final KakaoOAuthRestClient kakaoOAuthRestClient; + + public OAuthRestClient getOAuthRestClient(Provider provider) { + switch (provider) { + case GOOGLE -> { + return googleOAuthRestClient; + } + case NAVER -> { + return naverOAuthRestClient; + } + case KAKAO -> { + return kakaoOAuthRestClient; + } + default -> throw AuthenticationErrorCode.INVALID_PROVIDER.toException(); + } + } +} From 440aa71383298bbd6e7c181cf68af2cd38a71433 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 03:55:56 +0900 Subject: [PATCH 018/478] =?UTF-8?q?feat:=20(#13)=20Google,=20Naver,=20Kaka?= =?UTF-8?q?o=EC=9D=98=20RestClient=20=EA=B5=AC=ED=98=84=EC=B2=B4=EB=A5=BC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../google/GoogleOAuthRestClient.java | 89 ++++++++++++++ .../kakao/KakaoOAuthRestClient.java | 112 ++++++++++++++++++ .../kakao/dto/KakaoResourceResponse.java | 26 ++++ .../naver/NaverOAuthRestClient.java | 110 +++++++++++++++++ .../naver/dto/NaverResourceResponse.java | 16 +++ 5 files changed, 353 insertions(+) create mode 100644 src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/kakao/dto/KakaoResourceResponse.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/naver/dto/NaverResourceResponse.java diff --git a/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java new file mode 100644 index 000000000..76b09c1ba --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java @@ -0,0 +1,89 @@ +package spring.backend.auth.infrastructure.google; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; +import spring.backend.auth.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.OAuthRestClient; +import spring.backend.core.configuration.property.GoogleOAuthProperty; + +import java.net.URI; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class GoogleOAuthRestClient implements OAuthRestClient { + + private static final String GRANT_TYPE = "authorization_code"; + + private final GoogleOAuthProperty googleOAuthProperty; + + @Override + public URI getAuthUrl() { + return UriComponentsBuilder.fromUriString(googleOAuthProperty.getAuthUri()) + .queryParam("client_id", googleOAuthProperty.getClientId()) + .queryParam("redirect_uri", googleOAuthProperty.getRedirectUri()) + .queryParam("response_type", "code") + .queryParam("scope", String.join(" ", googleOAuthProperty.getScope())) + .build() + .toUri(); + } + + @Override + public OAuthAccessTokenResponse getAccessToken(String authCode, String state) { + if (authCode == null || authCode.isEmpty()) { + log.error("[GoogleOAuthRestClient] authCode is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + return RestClient.create() + .post() + .uri(googleOAuthProperty.getTokenUri()) + .headers(header -> header.setContentType(MediaType.APPLICATION_FORM_URLENCODED)) + .body(createAccessTokenRequestBody(authCode)) + .retrieve() + .body(OAuthAccessTokenResponse.class); + } catch (Exception e) { + log.error("[GoogleOAuthRestClient] error", e); + throw AuthenticationErrorCode.ACCESS_TOKEN_NOT_ISSUED.toException(); + } + } + + @Override + public OAuthResourceResponse getResource(String oauthToken) { + if (oauthToken == null || oauthToken.isEmpty()) { + log.error("[GoogleOAuthRestClient] oauthToken is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + return RestClient.create() + .get() + .uri(googleOAuthProperty.getResourceUri()) + .headers(header -> { + header.setBearerAuth(oauthToken); + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + }) + .retrieve() + .body(OAuthResourceResponse.class); + } catch (Exception e) { + throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); + } + } + + private MultiValueMap createAccessTokenRequestBody(String authCode) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("client_id", googleOAuthProperty.getClientId()); + parameters.add("client_secret", googleOAuthProperty.getClientSecret()); + parameters.add("code", authCode); + parameters.add("grant_type", GRANT_TYPE); + parameters.add("redirect_uri", googleOAuthProperty.getRedirectUri()); + return parameters; + } +} diff --git a/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java new file mode 100644 index 000000000..c69fe3330 --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java @@ -0,0 +1,112 @@ +package spring.backend.auth.infrastructure.kakao; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; +import spring.backend.auth.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.OAuthRestClient; +import spring.backend.auth.infrastructure.kakao.dto.KakaoResourceResponse; +import spring.backend.core.configuration.property.KakaoOAuthProperty; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class KakaoOAuthRestClient implements OAuthRestClient { + + private static final String GRANT_TYPE = "authorization_code"; + + private final KakaoOAuthProperty kakaoOAuthProperty; + + @Override + public URI getAuthUrl() { + return UriComponentsBuilder.fromUriString(kakaoOAuthProperty.getAuthUri()) + .queryParam("client_id", kakaoOAuthProperty.getClientId()) + .queryParam("redirect_uri", kakaoOAuthProperty.getRedirectUri()) + .queryParam("response_type", "code") + .queryParam("scope", String.join(" ", kakaoOAuthProperty.getScope())) + .build() + .toUri(); + } + + @Override + public OAuthAccessTokenResponse getAccessToken(String authCode, String state) { + if (authCode == null || authCode.isEmpty()) { + log.error("[KakaoOAuthRestClient] authCode is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + return RestClient.create() + .post() + .uri(kakaoOAuthProperty.getTokenUri()) + .headers(header -> { + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); + }) + .body(createAccessTokenRequestBody(authCode)) + .retrieve() + .body(OAuthAccessTokenResponse.class); + } catch (Exception e) { + log.error("[GoogleOAuthRestClient] error", e); + throw AuthenticationErrorCode.ACCESS_TOKEN_NOT_ISSUED.toException(); + } + } + + @Override + public OAuthResourceResponse getResource(String oauthToken) { + if (oauthToken == null || oauthToken.isEmpty()) { + log.error("[KakaoOAuthRestClient] oauthToken is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + KakaoResourceResponse kakaoResourceResponse = RestClient.create() + .get() + .uri(kakaoOAuthProperty.getResourceUri()) + .headers(header -> { + header.setBearerAuth(oauthToken); + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); + }) + .retrieve() + .body(KakaoResourceResponse.class); + Long id = Optional.ofNullable(kakaoResourceResponse) + .map(KakaoResourceResponse::getId) + .orElseThrow(AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE::toException); + KakaoResourceResponse.Response kakaoAccount = Optional.ofNullable(kakaoResourceResponse.getKakaoAccount()) + .orElseThrow(AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE::toException); + String email = Optional.ofNullable(kakaoAccount.getEmail()) + .orElseThrow(AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE::toException); + String nickname = Optional.ofNullable(kakaoAccount.getProfile()) + .map(KakaoResourceResponse.Response.Profile::getNickname) + .orElseThrow(AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE::toException); + return OAuthResourceResponse.builder() + .id(String.valueOf(id)) + .name(nickname) + .email(email) + .build(); + } catch (Exception e) { + throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); + } + } + + private MultiValueMap createAccessTokenRequestBody(String authCode) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("client_id", kakaoOAuthProperty.getClientId()); + parameters.add("client_secret", kakaoOAuthProperty.getClientSecret()); + parameters.add("code", authCode); + parameters.add("grant_type", GRANT_TYPE); + parameters.add("redirect_uri", kakaoOAuthProperty.getRedirectUri()); + return parameters; + } +} diff --git a/src/main/java/spring/backend/auth/infrastructure/kakao/dto/KakaoResourceResponse.java b/src/main/java/spring/backend/auth/infrastructure/kakao/dto/KakaoResourceResponse.java new file mode 100644 index 000000000..ff61703a8 --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/kakao/dto/KakaoResourceResponse.java @@ -0,0 +1,26 @@ +package spring.backend.auth.infrastructure.kakao.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class KakaoResourceResponse { + + private Long id; + + @JsonProperty("kakao_account") + private Response kakaoAccount; + + @Getter + public static class Response { + + private String email; + + private Profile profile; + + @Getter + public static class Profile { + private String nickname; + } + } +} diff --git a/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java new file mode 100644 index 000000000..9ad948fde --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java @@ -0,0 +1,110 @@ +package spring.backend.auth.infrastructure.naver; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; +import spring.backend.auth.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.OAuthRestClient; +import spring.backend.auth.infrastructure.naver.dto.NaverResourceResponse; +import spring.backend.core.configuration.property.NaverOAuthProperty; + +import java.math.BigInteger; +import java.net.URI; +import java.security.SecureRandom; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class NaverOAuthRestClient implements OAuthRestClient { + + private static final String GRANT_TYPE = "authorization_code"; + + private final NaverOAuthProperty naverOAuthProperty; + + @Override + public URI getAuthUrl() { + return UriComponentsBuilder.fromUriString(naverOAuthProperty.getAuthUri()) + .queryParam("client_id", naverOAuthProperty.getClientId()) + .queryParam("redirect_uri", naverOAuthProperty.getRedirectUri()) + .queryParam("response_type", "code") + .queryParam("state", generateState()) + .build() + .toUri(); + } + + @Override + public OAuthAccessTokenResponse getAccessToken(String authCode, String state) { + if (authCode == null || authCode.isEmpty() || state == null || state.isEmpty()) { + log.error("[NaverOAuthProperty] authCode is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + return RestClient.create() + .post() + .uri(naverOAuthProperty.getTokenUri()) + .headers(header -> header.setContentType(MediaType.APPLICATION_FORM_URLENCODED)) + .body(createAccessTokenRequestBody(authCode, state)) + .retrieve() + .body(OAuthAccessTokenResponse.class); + } catch (Exception e) { + log.error("[NaverOAuthProperty] error", e); + throw AuthenticationErrorCode.ACCESS_TOKEN_NOT_ISSUED.toException(); + } + } + + @Override + public OAuthResourceResponse getResource(String oauthToken) { + if (oauthToken == null || oauthToken.isEmpty()) { + log.error("[NaverOAuthProperty] oauthToken is null"); + throw AuthenticationErrorCode.NOT_EXIST_AUTH_CODE.toException(); + } + try { + NaverResourceResponse naverResourceResponse = RestClient.create() + .get() + .uri(naverOAuthProperty.getResourceUri()) + .headers(header -> { + header.setBearerAuth(oauthToken); + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + }) + .retrieve() + .body(NaverResourceResponse.class); + + if (naverResourceResponse == null) { + throw AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE.toException(); + } + NaverResourceResponse.Response resourceResponse = naverResourceResponse.getResponse(); + if (resourceResponse == null || resourceResponse.getId() == null || resourceResponse.getName() == null || resourceResponse.getEmail() == null) { + throw AuthenticationErrorCode.NOT_EXIST_RESOURCE_RESPONSE.toException(); + } + return OAuthResourceResponse.builder() + .id(resourceResponse.getId()) + .name(resourceResponse.getName()) + .email(resourceResponse.getEmail()) + .build(); + } catch (Exception e) { + throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); + } + } + + private String generateState() { + SecureRandom random = new SecureRandom(); + return new BigInteger(130, random).toString(32); + } + + private MultiValueMap createAccessTokenRequestBody(String authCode, String state) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("client_id", naverOAuthProperty.getClientId()); + parameters.add("client_secret", naverOAuthProperty.getClientSecret()); + parameters.add("grant_type", GRANT_TYPE); + parameters.add("state", state); + parameters.add("code", authCode); + return parameters; + } +} diff --git a/src/main/java/spring/backend/auth/infrastructure/naver/dto/NaverResourceResponse.java b/src/main/java/spring/backend/auth/infrastructure/naver/dto/NaverResourceResponse.java new file mode 100644 index 000000000..2759b2027 --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/naver/dto/NaverResourceResponse.java @@ -0,0 +1,16 @@ +package spring.backend.auth.infrastructure.naver.dto; + +import lombok.Getter; + +@Getter +public class NaverResourceResponse { + + private Response response; + + @Getter + public static class Response { + private String id; + private String name; + private String email; + } +} From 9214235bed28c98cb0b9f65e257513bf46f88e25 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 03:57:48 +0900 Subject: [PATCH 019/478] =?UTF-8?q?feat:=20(#13)=20=EC=8D=A8=EB=93=9C?= =?UTF-8?q?=ED=8C=8C=ED=8B=B0=EC=97=90=20=EC=9D=B8=EA=B0=80=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=B0=9B=EC=95=84=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/AuthorizeOAuthService.java | 29 +++++++++++++++++++ .../AuthorizeOAuthController.java | 25 ++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/spring/backend/auth/application/AuthorizeOAuthService.java create mode 100644 src/main/java/spring/backend/auth/presentation/AuthorizeOAuthController.java diff --git a/src/main/java/spring/backend/auth/application/AuthorizeOAuthService.java b/src/main/java/spring/backend/auth/application/AuthorizeOAuthService.java new file mode 100644 index 000000000..289a96f2d --- /dev/null +++ b/src/main/java/spring/backend/auth/application/AuthorizeOAuthService.java @@ -0,0 +1,29 @@ +package spring.backend.auth.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.OAuthRestClient; +import spring.backend.auth.infrastructure.OAuthRestClientFactory; +import spring.backend.member.domain.value.Provider; + +import java.net.URI; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class AuthorizeOAuthService { + + private final OAuthRestClientFactory oAuthRestClientFactory; + + public URI getAuthorizeUrl(String providerName) { + if (providerName == null || providerName.isEmpty()) { + log.error("[AuthorizeOAuthService] Invalid provider name"); + throw AuthenticationErrorCode.NOT_EXIST_PROVIDER.toException(); + } + Provider provider = Provider.valueOf(providerName.toUpperCase()); + OAuthRestClient oAuthRestClient = oAuthRestClientFactory.getOAuthRestClient(provider); + return oAuthRestClient.getAuthUrl(); + } +} diff --git a/src/main/java/spring/backend/auth/presentation/AuthorizeOAuthController.java b/src/main/java/spring/backend/auth/presentation/AuthorizeOAuthController.java new file mode 100644 index 000000000..cde03957b --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/AuthorizeOAuthController.java @@ -0,0 +1,25 @@ +package spring.backend.auth.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.view.RedirectView; +import spring.backend.auth.application.AuthorizeOAuthService; + +import java.net.URI; + +@Controller +@RequestMapping("/v1/oauth") +@RequiredArgsConstructor +public class AuthorizeOAuthController { + + private final AuthorizeOAuthService authorizeOAuthService; + + @GetMapping("/{providerName}") + public RedirectView authorizeOAuth(@PathVariable String providerName) { + URI authUrl = authorizeOAuthService.getAuthorizeUrl(providerName); + return new RedirectView(authUrl.toASCIIString()); + } +} From ecae71d30ad295ea14f5b3da8ce0e630cdf768b7 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:00:57 +0900 Subject: [PATCH 020/478] =?UTF-8?q?feat:=20(#13)=20=EC=8D=A8=EB=93=9C?= =?UTF-8?q?=ED=8C=8C=ED=8B=B0=EC=97=90=EC=84=9C=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=ED=81=B0=EA=B3=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=B3=B4=EC=A1=B0=ED=9A=8C=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20DTO=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/OAuthAccessTokenResponse.java | 20 +++++++++++++++++++ .../dto/response/OAuthResourceResponse.java | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java create mode 100644 src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java diff --git a/src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java b/src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java new file mode 100644 index 000000000..07146cc02 --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java @@ -0,0 +1,20 @@ +package spring.backend.auth.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class OAuthAccessTokenResponse { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("expires_in") + private long expiresIn; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("token_type") + private String tokenType; +} diff --git a/src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java b/src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java new file mode 100644 index 000000000..1cb2bf3ae --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java @@ -0,0 +1,19 @@ +package spring.backend.auth.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class OAuthResourceResponse { + + private String id; + + private String email; + + @JsonProperty("verified_email") + private boolean verifiedEmail; + + private String name; +} From 3a7b27a301df2f80f84b5fd15c8cd502464ac98a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:06:04 +0900 Subject: [PATCH 021/478] =?UTF-8?q?feat:=20(#13)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A4=91=EB=B3=B5=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 57 +++++++++++++++++++ .../auth/dto/response/LoginResponse.java | 4 ++ .../HandleOAuthLoginController.java | 24 ++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java create mode 100644 src/main/java/spring/backend/auth/dto/response/LoginResponse.java create mode 100644 src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java new file mode 100644 index 000000000..8fb17effb --- /dev/null +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -0,0 +1,57 @@ +package spring.backend.auth.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.auth.dto.response.LoginResponse; +import spring.backend.auth.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.OAuthRestClient; +import spring.backend.auth.infrastructure.OAuthRestClientFactory; +import spring.backend.member.application.CreateMemberWithOAuthService; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Provider; +import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class HandleOAuthLoginService { + + private final OAuthRestClientFactory oAuthRestClientFactory; + + private final CreateMemberWithOAuthService createMemberWithOAuthService; + + public LoginResponse handleOAuthLogin(String providerName, String code, String state) { + if (providerName == null || providerName.isEmpty()) { + throw AuthenticationErrorCode.NOT_EXIST_PROVIDER.toException(); + } + Provider provider = Provider.valueOf(providerName.toUpperCase()); + OAuthRestClient oAuthRestClient = oAuthRestClientFactory.getOAuthRestClient(provider); + + OAuthAccessTokenResponse oAuthAccessTokenResponse = oAuthRestClient.getAccessToken(code, state); + if (oAuthAccessTokenResponse == null) { + log.error("[HandleOAuthLoginService] OAuth access token could not be retrieved."); + throw AuthenticationErrorCode.ACCESS_TOKEN_NOT_ISSUED.toException(); + } + OAuthResourceResponse oAuthResourceResponse = oAuthRestClient.getResource(oAuthAccessTokenResponse.getAccessToken()); + if (oAuthResourceResponse == null) { + log.error("[HandleOAuthLoginService] OAuth resource could not be retrieved."); + throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); + } + + CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() + .provider(provider) + .email(oAuthResourceResponse.getEmail()) + .nickname(oAuthResourceResponse.getName()) + .build(); + + Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); + + /** + * todo: 사용자 정보를 가지고 AccessToken, RefreshToken을 생성한다. + */ + return null; + } +} diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java new file mode 100644 index 000000000..29642109a --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java @@ -0,0 +1,4 @@ +package spring.backend.auth.dto.response; + +public record LoginResponse(String accessToken, String refreshToken) { +} diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java new file mode 100644 index 000000000..936dfbe38 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -0,0 +1,24 @@ +package spring.backend.auth.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import spring.backend.auth.application.HandleOAuthLoginService; +import spring.backend.auth.dto.response.LoginResponse; +import spring.backend.core.presentation.RestResponse; + +@RestController +@RequestMapping("/v1/oauth/login") +@RequiredArgsConstructor +public class HandleOAuthLoginController { + + private final HandleOAuthLoginService handleOAuthLoginService; + + @GetMapping("/{providerName}") + public ResponseEntity> handleOAuthLogin(@RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state", required = false) String state, + @PathVariable String providerName) { + LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); + return ResponseEntity.ok(new RestResponse<>(loginResponse)); + } +} From 2188dac54db1e2144d29b30e1a75ad325f3f4723 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:32:34 +0900 Subject: [PATCH 022/478] =?UTF-8?q?feat:=20(#15)=20BaseEntity=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EA=B3=A0,=20EnableJpaAuditing?= =?UTF-8?q?=EC=9D=84=20=ED=99=9C=EC=84=B1=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/BackendApplication.java | 2 ++ .../infrastructure/jpa/shared/BaseEntity.java | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java diff --git a/src/main/java/spring/backend/BackendApplication.java b/src/main/java/spring/backend/BackendApplication.java index 5b9288112..5fe6517dd 100644 --- a/src/main/java/spring/backend/BackendApplication.java +++ b/src/main/java/spring/backend/BackendApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class BackendApplication { diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java new file mode 100644 index 000000000..1cf61e76f --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java @@ -0,0 +1,32 @@ +package spring.backend.core.infrastructure.jpa.shared; + +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@MappedSuperclass +@EntityListeners(value = AuditingEntityListener.class) +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + protected UUID id; + + @CreatedDate + protected LocalDateTime createdAt; + + @LastModifiedDate + protected LocalDateTime updatedAt; + + @Builder.Default + protected Boolean deleted = false; +} \ No newline at end of file From d71fc389a73bfede539fd573c25e24c13ed9a674 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:34:50 +0900 Subject: [PATCH 023/478] =?UTF-8?q?feat:=20(#15)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EA=B3=BC=20=EB=B0=B8=EB=A5=98=20=ED=83=80=EC=9E=85=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Member.java | 48 +++++++++++++++++++ .../backend/member/domain/value/Provider.java | 8 ++++ .../backend/member/domain/value/Role.java | 8 ++++ 3 files changed, 64 insertions(+) create mode 100644 src/main/java/spring/backend/member/domain/entity/Member.java create mode 100644 src/main/java/spring/backend/member/domain/value/Provider.java create mode 100644 src/main/java/spring/backend/member/domain/value/Role.java diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java new file mode 100644 index 000000000..2f06035e6 --- /dev/null +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -0,0 +1,48 @@ +package spring.backend.member.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import spring.backend.member.domain.value.Provider; +import spring.backend.member.domain.value.Role; +import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Builder +public class Member { + + private final UUID id; + + private final Provider provider; + + private final Role role; + + private final String email; + + private final String nickname; + + private final LocalDateTime createdAt; + + private final LocalDateTime updatedAt; + + private final Boolean deleted; + + public static Member toDomainEntity(MemberJpaEntity memberJpaEntity) { + return Member.builder() + .id(memberJpaEntity.getId()) + .provider(memberJpaEntity.getProvider()) + .role(memberJpaEntity.getRole()) + .email(memberJpaEntity.getEmail()) + .nickname(memberJpaEntity.getNickname()) + .createdAt(memberJpaEntity.getCreatedAt()) + .updatedAt(memberJpaEntity.getUpdatedAt()) + .deleted(memberJpaEntity.getDeleted()) + .build(); + } + + public boolean isSameProvider(Provider otherProvider) { + return this.provider.equals(otherProvider); + } +} diff --git a/src/main/java/spring/backend/member/domain/value/Provider.java b/src/main/java/spring/backend/member/domain/value/Provider.java new file mode 100644 index 000000000..7c5e8cdce --- /dev/null +++ b/src/main/java/spring/backend/member/domain/value/Provider.java @@ -0,0 +1,8 @@ +package spring.backend.member.domain.value; + +import lombok.Getter; + +@Getter +public enum Provider { + GOOGLE, NAVER, KAKAO +} diff --git a/src/main/java/spring/backend/member/domain/value/Role.java b/src/main/java/spring/backend/member/domain/value/Role.java new file mode 100644 index 000000000..c9b892204 --- /dev/null +++ b/src/main/java/spring/backend/member/domain/value/Role.java @@ -0,0 +1,8 @@ +package spring.backend.member.domain.value; + +import lombok.Getter; + +@Getter +public enum Role { + MEMBER, GUEST +} From 9273459ba03e8dcef88b124167e6fcf8c12a1f1c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:35:26 +0900 Subject: [PATCH 024/478] =?UTF-8?q?feat:=20(#15)=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B3=84=EC=B8=B5=EC=9D=98=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=84=B0=EB=A6=AC=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/domain/repository/MemberRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/spring/backend/member/domain/repository/MemberRepository.java diff --git a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java new file mode 100644 index 000000000..d41b0c885 --- /dev/null +++ b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package spring.backend.member.domain.repository; + +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +public interface MemberRepository { + + Member findById(UUID id); + void save(Member member); + Member findByEmail(String email); +} From 9b12185dbc454545d3aea20b0dac60ce1a356777 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:36:01 +0900 Subject: [PATCH 025/478] =?UTF-8?q?feat:=20(#15)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/exception/MemberErrorCode.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/spring/backend/member/exception/MemberErrorCode.java diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java new file mode 100644 index 000000000..673b7da2c --- /dev/null +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -0,0 +1,25 @@ +package spring.backend.member.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum MemberErrorCode implements BaseErrorCode { + + NOT_EXIST_CONDITION(HttpStatus.BAD_REQUEST, "요청 조건이 존재하지 않습니다."), + ALREADY_EXIST_EMAIL(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일의 회원이 있습니다."), + MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From 645e275b0ef3d206e4a0fa3be03fdc9311ad1060 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 04:46:46 +0900 Subject: [PATCH 026/478] =?UTF-8?q?feat:=20(#15)=20OAuth=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMemberWithOAuthService.java | 41 +++++++++++++++++++ .../request/CreateMemberWithOAuthRequest.java | 16 ++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java create mode 100644 src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java new file mode 100644 index 000000000..1a17a28a2 --- /dev/null +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -0,0 +1,41 @@ +package spring.backend.member.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Role; +import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; +import spring.backend.member.exception.MemberErrorCode; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class CreateMemberWithOAuthService { + + private final MemberRepository memberRepository; + + public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { + if (request == null) { + log.error("[createMemberWithOAuth] request is null"); + throw MemberErrorCode.NOT_EXIST_CONDITION.toException(); + } + Member member = memberRepository.findByEmail(request.getEmail()); + if (member == null) { + Member newMember = Member.builder() + .provider(request.getProvider()) + .role(Role.GUEST) + .email(request.getEmail()) + .nickname(request.getNickname()) + .build(); + memberRepository.save(newMember); + return newMember; + } + if (!member.isSameProvider(request.getProvider())) { + log.error("[createMemberWithOAuth] provider mismatch"); + throw MemberErrorCode.ALREADY_EXIST_EMAIL.toException(); + } + return member; + } +} diff --git a/src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java b/src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java new file mode 100644 index 000000000..f8d88f263 --- /dev/null +++ b/src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java @@ -0,0 +1,16 @@ +package spring.backend.member.dto.request; + +import lombok.Builder; +import lombok.Getter; +import spring.backend.member.domain.value.Provider; + +@Getter +@Builder +public class CreateMemberWithOAuthRequest { + + private final Provider provider; + + private final String email; + + private final String nickname; +} From d5fd7639238d2336f903d133ac02b403698ba9d7 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 05:14:04 +0900 Subject: [PATCH 027/478] =?UTF-8?q?feat:=20(#15)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20JPA=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=EA=B3=BC=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=ED=95=98=EB=8A=94=20=EB=A7=A4=ED=8D=BC?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/mapper/MemberMapper.java | 17 +++++++ .../jpa/entity/MemberJpaEntity.java | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/main/java/spring/backend/member/infrastructure/mapper/MemberMapper.java create mode 100644 src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java diff --git a/src/main/java/spring/backend/member/infrastructure/mapper/MemberMapper.java b/src/main/java/spring/backend/member/infrastructure/mapper/MemberMapper.java new file mode 100644 index 000000000..1a9ba2817 --- /dev/null +++ b/src/main/java/spring/backend/member/infrastructure/mapper/MemberMapper.java @@ -0,0 +1,17 @@ +package spring.backend.member.infrastructure.mapper; + +import org.springframework.stereotype.Component; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; + +@Component +public class MemberMapper { + + public Member toDomainEntity(MemberJpaEntity member) { + return Member.toDomainEntity(member); + } + + public MemberJpaEntity toJpaEntity(Member member) { + return MemberJpaEntity.toJpaEntity(member); + } +} diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java new file mode 100644 index 000000000..ff0db6b17 --- /dev/null +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java @@ -0,0 +1,47 @@ +package spring.backend.member.infrastructure.persistence.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import spring.backend.core.infrastructure.jpa.shared.BaseEntity; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Provider; +import spring.backend.member.domain.value.Role; + +import java.util.Optional; + +@Entity +@Table(name = "member") +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberJpaEntity extends BaseEntity { + + @Enumerated(EnumType.STRING) + private Provider provider; + + @Enumerated(EnumType.STRING) + private Role role; + + private String email; + + private String nickname; + + public static MemberJpaEntity toJpaEntity(Member member) { + return MemberJpaEntity.builder() + .id(member.getId()) + .provider(member.getProvider()) + .role(member.getRole()) + .email(member.getEmail()) + .nickname(member.getNickname()) + .createdAt(member.getCreatedAt()) + .updatedAt(member.getUpdatedAt()) + .deleted(Optional.ofNullable(member.getDeleted()).orElse(false)) + .build(); + } +} From 5575e788811311b718cfd8e234eaadf1ca77dcd3 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 05:15:09 +0900 Subject: [PATCH 028/478] =?UTF-8?q?feat:=20(#15)=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EA=B3=84=EC=B8=B5=EC=97=90=20JpaRepository=EC=99=80?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=EC=B2=B4=EC=9D=B8=20=EC=96=B4=EB=8C=91?= =?UTF-8?q?=ED=84=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/adapter/MemberRepositoryImpl.java | 49 +++++++++++++++++++ .../jpa/repository/MemberJpaRepository.java | 11 +++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java create mode 100644 src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java new file mode 100644 index 000000000..298e262d8 --- /dev/null +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java @@ -0,0 +1,49 @@ +package spring.backend.member.infrastructure.persistence.jpa.adapter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Repository; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.exception.MemberErrorCode; +import spring.backend.member.infrastructure.mapper.MemberMapper; +import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; +import spring.backend.member.infrastructure.persistence.jpa.repository.MemberJpaRepository; + +import java.util.UUID; + +@Repository +@RequiredArgsConstructor +@Log4j2 +public class MemberRepositoryImpl implements MemberRepository { + + private final MemberMapper memberMapper; + private final MemberJpaRepository memberJpaRepository; + + @Override + public Member findById(UUID id) { + MemberJpaEntity memberJpaEntity = memberJpaRepository.findById(id).orElse(null); + if (memberJpaEntity == null) { + return null; + } + return memberMapper.toDomainEntity(memberJpaEntity); + } + + @Override + public void save(Member member) { + MemberJpaEntity memberJpaEntity = memberMapper.toJpaEntity(member); + if (memberJpaEntity == null) { + throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); + } + memberJpaRepository.save(memberJpaEntity); + } + + @Override + public Member findByEmail(String email) { + MemberJpaEntity memberJpaEntity = memberJpaRepository.findByEmail(email); + if (memberJpaEntity == null) { + return null; + } + return memberMapper.toDomainEntity(memberJpaEntity); + } +} diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java new file mode 100644 index 000000000..7c6233965 --- /dev/null +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java @@ -0,0 +1,11 @@ +package spring.backend.member.infrastructure.persistence.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; + +import java.util.UUID; + +public interface MemberJpaRepository extends JpaRepository { + + MemberJpaEntity findByEmail(String email); +} From 2749ed7999c8ac65ca239d4c52433a4880b16bef Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 14:02:32 +0900 Subject: [PATCH 029/478] =?UTF-8?q?feat:=20(#15)=20=EA=B2=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=9A=8C=EC=9B=90=20=EC=A0=80=EC=9E=A5=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=ED=9A=8C=EC=9B=90=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80,=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B3=B5=EA=B8=89=EC=9E=90=20=EB=B9=84=EA=B5=90=20=EB=93=B1?= =?UTF-8?q?=EC=9D=98=20=EC=A1=B0=EA=B1=B4=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMemberWithOAuthService.java | 36 ++++++++++++------- .../backend/member/domain/entity/Member.java | 13 +++++++ .../domain/repository/MemberRepository.java | 2 ++ .../member/exception/MemberErrorCode.java | 2 +- .../jpa/adapter/MemberRepositoryImpl.java | 11 ++++++ .../jpa/repository/MemberJpaRepository.java | 3 ++ 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java index 1a17a28a2..0adb15923 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -5,10 +5,11 @@ import org.springframework.stereotype.Service; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; -import spring.backend.member.domain.value.Role; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; import spring.backend.member.exception.MemberErrorCode; +import java.util.List; + @Service @RequiredArgsConstructor @Log4j2 @@ -21,21 +22,30 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { log.error("[createMemberWithOAuth] request is null"); throw MemberErrorCode.NOT_EXIST_CONDITION.toException(); } - Member member = memberRepository.findByEmail(request.getEmail()); - if (member == null) { - Member newMember = Member.builder() - .provider(request.getProvider()) - .role(Role.GUEST) - .email(request.getEmail()) - .nickname(request.getNickname()) - .build(); + List members = memberRepository.findAllByEmail(request.getEmail()); + if (members == null || members.isEmpty()) { + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); memberRepository.save(newMember); return newMember; } - if (!member.isSameProvider(request.getProvider())) { - log.error("[createMemberWithOAuth] provider mismatch"); - throw MemberErrorCode.ALREADY_EXIST_EMAIL.toException(); + Member existingMember = members.stream() + .filter(Member::isMember) + .findFirst() + .orElse(null); + if (existingMember != null) { + if (!existingMember.isSameProvider(request.getProvider())) { + log.error("[CreateMemberWithOAuthService] member already exists with a different provider [{}]", existingMember.getProvider()); + throw MemberErrorCode.ALREADY_REGISTERED_WITH_DIFFERENT_OAUTH2.toException(); + } + return existingMember; } - return member; + return members.stream() + .filter(m -> m.isSameProvider(request.getProvider())) + .findFirst() + .orElseGet(() -> { + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); + memberRepository.save(newMember); + return newMember; + }); } } diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 2f06035e6..1ce438b97 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -45,4 +45,17 @@ public static Member toDomainEntity(MemberJpaEntity memberJpaEntity) { public boolean isSameProvider(Provider otherProvider) { return this.provider.equals(otherProvider); } + + public boolean isMember() { + return Role.MEMBER.equals(this.role); + } + + public static Member createGuestMember(Provider provider, String email, String nickname) { + return Member.builder() + .provider(provider) + .role(Role.GUEST) + .email(email) + .nickname(nickname) + .build(); + } } diff --git a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java index d41b0c885..bbb0d9f45 100644 --- a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java +++ b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java @@ -2,6 +2,7 @@ import spring.backend.member.domain.entity.Member; +import java.util.List; import java.util.UUID; public interface MemberRepository { @@ -9,4 +10,5 @@ public interface MemberRepository { Member findById(UUID id); void save(Member member); Member findByEmail(String email); + List findAllByEmail(String email); } diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index 673b7da2c..b4065ea25 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -11,7 +11,7 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_EXIST_CONDITION(HttpStatus.BAD_REQUEST, "요청 조건이 존재하지 않습니다."), - ALREADY_EXIST_EMAIL(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일의 회원이 있습니다."), + ALREADY_REGISTERED_WITH_DIFFERENT_OAUTH2(HttpStatus.BAD_REQUEST, "이미 다른 소셜 로그인으로 가입된 계정입니다."), MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java index 298e262d8..5c6692c72 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java @@ -10,7 +10,9 @@ import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; import spring.backend.member.infrastructure.persistence.jpa.repository.MemberJpaRepository; +import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Repository @RequiredArgsConstructor @@ -46,4 +48,13 @@ public Member findByEmail(String email) { } return memberMapper.toDomainEntity(memberJpaEntity); } + + @Override + public List findAllByEmail(String email) { + List memberJpaEntities = memberJpaRepository.findAllByEmail(email); + if (memberJpaEntities == null || memberJpaEntities.isEmpty()) { + return null; + } + return memberJpaEntities.stream().map(memberMapper::toDomainEntity).collect(Collectors.toList()); + } } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java index 7c6233965..34d73ea8b 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java @@ -3,9 +3,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; +import java.util.List; import java.util.UUID; public interface MemberJpaRepository extends JpaRepository { MemberJpaEntity findByEmail(String email); + + List findAllByEmail(String email); } From 04bb61a38ecaf7a79ef7639cfca42a2f4a0a8572 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 14 Oct 2024 14:59:29 +0900 Subject: [PATCH 030/478] =?UTF-8?q?chore:=20(#13)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20Spring=20Security,=20?= =?UTF-8?q?OAuth=20Client=20dependency=20=EC=A0=9C=EA=B1=B0=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle b/build.gradle index e1f166d47..843d4300c 100644 --- a/build.gradle +++ b/build.gradle @@ -39,10 +39,6 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - // Spring Security & OAuth2 - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - // Lombok (code simplification) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' From 4d3c5f9137f3e6fee037a56cd3bd5f0e11064e83 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Oct 2024 13:03:47 +0900 Subject: [PATCH 031/478] =?UTF-8?q?chore:=20(#8)=20CI/CD=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85=EC=9D=84=20=EC=9C=84=ED=95=B4=20yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 0 .github/workflows/ci.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..e69de29bb From 2de79c2040bb343caab8626fbef9fa98bdc14d5c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Oct 2024 13:03:47 +0900 Subject: [PATCH 032/478] =?UTF-8?q?chore:=20(#8)=20CI/CD=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20.dockerignore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 4 ++++ .gitignore | 1 + 2 files changed, 5 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..4ca6a34f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git/ +.gitignore/ +.idea/ +build/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5b895ba31..d6ddb0195 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ out/ ### Configuration ### src/main/resources/application-*.yml +src/test/resources/application.yml \ No newline at end of file From a780b88290c9ceab03f79f5caf5c0abd36bcfda2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Oct 2024 13:03:47 +0900 Subject: [PATCH 033/478] =?UTF-8?q?chore:=20(#8)=20CI/CD=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 22 ++++++++++ .github/workflows/ci.yml | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e69de29bb..da945950e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -0,0 +1,22 @@ +name: Cnergy Backend CD + +on: + push: + branches: [dev] + +jobs: + cd: + runs-on: ubuntu-latest + + steps: + - name: NCP 서버 접속 및 배포 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USERNAME }} + password: ${{ secrets.NCP_PASSWORD }} + port: ${{ secrets.NCP_PORT }} + script: | + sudo chmod +x ./deploy.sh + export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} + ./deploy.sh \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29bb..dcd5f73a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: Cnergy Backend CI + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize] + branches: [dev] + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Gradle 캐시 적용 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: JDK 17 세팅 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: YML 파일 세팅 + env: + APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} + TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} + run: | + cd ./src + rm -rf main/resources/application.yml + mkdir -p test/resources + echo "$APPLICATION_PROPERTIES" > main/resources/application.yml + echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml + + - name: gradlew 권한 부여 + run: chmod +x gradlew + + - name: 테스트 수행 + run: ./gradlew test + + - name: 스프링부트 빌드 + run: ./gradlew build + + - name: Docker Buildx 세팅 + uses: docker/setup-buildx-action@v3 + + - name: NCP 레지스트리 로그인 + uses: docker/login-action@v3 + with: + registry: ${{ secrets.NCP_CONTAINER_REGISTRY }} + username: ${{ secrets.NCP_ACCESS_KEY }} + password: ${{ secrets.NCP_SECRET_KEY }} + + - name: 도커 이미지 태그 생성 + run: | + # github.ref_name에서 슬래시를 대시로 변환 + IMAGE_TAG=$(echo "${{ github.ref_name }}-${{ github.run_number }}" | sed 's/\//-/g') + echo "IMAGE_TAG=$IMAGE_TAG" > image_tag.txt + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + + - name: 도커 이미지 빌드 후 푸시 + if: success() + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ env.IMAGE_TAG }} + platforms: linux/amd64,linux/arm64 + + - name: NCP 접속 후 이미지 다운로드 + if: success() + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USERNAME }} + password: ${{ secrets.NCP_PASSWORD }} + port: ${{ secrets.NCP_PORT }} + script: | + docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ env.IMAGE_TAG }} \ No newline at end of file From 423a98120f32c36088140fa469ea2478b5bc2923 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Oct 2024 13:03:47 +0900 Subject: [PATCH 034/478] =?UTF-8?q?chore:=20(#8)=20CI/CD=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20-=20test=20=EC=9A=A9=20h2=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20build.gradle=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 843d4300c..45843bd60 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'com.h2database:h2' // Development tools (for local development only) developmentOnly 'org.springframework.boot:spring-boot-devtools' From 5db18cd96d73866278e06e3b08400ca473e684af Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Oct 2024 13:03:47 +0900 Subject: [PATCH 035/478] =?UTF-8?q?chore:=20(#8)=20CI/CD=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=ED=95=9C=EB=8B=A4.=20-=20dev=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=EB=A1=9C=20merge=EA=B0=80=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=A0=EB=95=8C=EB=A7=8C=20CI/CD=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=EC=9D=B4=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 22 ---------------------- .github/workflows/{ci.yml => deploy.yml} | 23 +++++++++-------------- 2 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/cd.yml rename .github/workflows/{ci.yml => deploy.yml} (80%) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index da945950e..000000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Cnergy Backend CD - -on: - push: - branches: [dev] - -jobs: - cd: - runs-on: ubuntu-latest - - steps: - - name: NCP 서버 접속 및 배포 - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.NCP_HOST }} - username: ${{ secrets.NCP_USERNAME }} - password: ${{ secrets.NCP_PASSWORD }} - port: ${{ secrets.NCP_PORT }} - script: | - sudo chmod +x ./deploy.sh - export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} - ./deploy.sh \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/deploy.yml similarity index 80% rename from .github/workflows/ci.yml rename to .github/workflows/deploy.yml index dcd5f73a1..26978aee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/deploy.yml @@ -1,9 +1,7 @@ -name: Cnergy Backend CI +name: Cnergy Backend CI/CD on: - workflow_dispatch: - pull_request: - types: [opened, reopened, synchronize] + push: branches: [dev] jobs: @@ -60,13 +58,6 @@ jobs: username: ${{ secrets.NCP_ACCESS_KEY }} password: ${{ secrets.NCP_SECRET_KEY }} - - name: 도커 이미지 태그 생성 - run: | - # github.ref_name에서 슬래시를 대시로 변환 - IMAGE_TAG=$(echo "${{ github.ref_name }}-${{ github.run_number }}" | sed 's/\//-/g') - echo "IMAGE_TAG=$IMAGE_TAG" > image_tag.txt - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - - name: 도커 이미지 빌드 후 푸시 if: success() uses: docker/build-push-action@v6 @@ -74,10 +65,10 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ env.IMAGE_TAG }} + tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} platforms: linux/amd64,linux/arm64 - - name: NCP 접속 후 이미지 다운로드 + - name: NCP 접속 후 이미지 다운로드 및 배포 if: success() uses: appleboy/ssh-action@master with: @@ -86,4 +77,8 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | - docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ env.IMAGE_TAG }} \ No newline at end of file + docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} + sudo chmod +x ./deploy.sh + export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} + export GITHUB_SHA=${{ github.sha }} # GITHUB_SHA 변수를 deploy.sh에 전달 + ./deploy.sh \ No newline at end of file From 08550656b2e531bc5d09026a426f69426af70a53 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 00:53:40 +0900 Subject: [PATCH 036/478] =?UTF-8?q?feat:=20(#18)=20=EC=8A=A4=ED=94=84?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=EC=99=80=20JWT?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- build.gradle | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 26978aee9..067b6ddfe 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -80,5 +80,5 @@ jobs: docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} sudo chmod +x ./deploy.sh export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} - export GITHUB_SHA=${{ github.sha }} # GITHUB_SHA 변수를 deploy.sh에 전달 + export GITHUB_SHA=${{ github.sha }} ./deploy.sh \ No newline at end of file diff --git a/build.gradle b/build.gradle index 45843bd60..32cdf1446 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,14 @@ dependencies { // Swagger-UI implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { From 4422ceb4b7ccb3c2b47e3d4cd9b7b4439cbe4d10 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 01:25:22 +0900 Subject: [PATCH 037/478] =?UTF-8?q?feat:=20(#18)=20Access=20Token=20,=20Re?= =?UTF-8?q?feshToken=20=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AccessTokenProviderService.java | 9 +++++++++ .../auth/application/RefreshTokenProviderService.java | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/main/java/spring/backend/auth/application/AccessTokenProviderService.java create mode 100644 src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java diff --git a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java new file mode 100644 index 000000000..46384b9fd --- /dev/null +++ b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java @@ -0,0 +1,9 @@ +package spring.backend.auth.application; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class AccessTokenProviderService { +} diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java new file mode 100644 index 000000000..29834663f --- /dev/null +++ b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java @@ -0,0 +1,9 @@ +package spring.backend.auth.application; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class RefreshTokenProviderService { +} From cfab6a4b2c62bdeaa14c784510dff296b1d159ae Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 01:38:27 +0900 Subject: [PATCH 038/478] =?UTF-8?q?feat:=20(#18)=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=B8=EB=A5=98=20=ED=83=80=EC=9E=85=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/domain/jwt/value/Type.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/spring/backend/auth/domain/jwt/value/Type.java diff --git a/src/main/java/spring/backend/auth/domain/jwt/value/Type.java b/src/main/java/spring/backend/auth/domain/jwt/value/Type.java new file mode 100644 index 000000000..3268c2e52 --- /dev/null +++ b/src/main/java/spring/backend/auth/domain/jwt/value/Type.java @@ -0,0 +1,13 @@ +package spring.backend.auth.domain.jwt.value; + +import lombok.Getter; + +@Getter +public enum Type { + ACCESS("access"), REFRESH("refresh"); + private final String type; + + Type(String type) { + this.type = type; + } +} From 31cbe127ac1e0ecb4f2d06f9e81f12ac373f08bb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 01:41:29 +0900 Subject: [PATCH 039/478] =?UTF-8?q?feat:=20(#18)=20Access=20Token=20,=20Re?= =?UTF-8?q?feshToken=20=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20Static=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccessTokenProviderService.java | 25 ++++++++++++++++++ .../RefreshTokenProviderService.java | 26 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java index 46384b9fd..b615cb4ec 100644 --- a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java @@ -1,9 +1,34 @@ package spring.backend.auth.application; +import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import spring.backend.auth.domain.jwt.value.Type; +import spring.backend.member.domain.entity.Member; + +import javax.crypto.SecretKey; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; @Slf4j @Service public class AccessTokenProviderService { + private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); + + public static String accessTokenProvider(Member member) { + // 기한 설정 , 지금부터 1일 + Date expiryDate = Date.from( + Instant.now().plus(1, ChronoUnit.DAYS) + ); + + // AccessToken 생성 + return Jwts.builder() + .subject(member.getEmail()) // 추후 변경 가능 + .claim("type", Type.ACCESS.getType()) + .issuedAt(new Date()) + .expiration(expiryDate) + .signWith(SECRET_KEY) + .compact(); + } } diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java index 29834663f..ac241ade8 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java @@ -1,9 +1,35 @@ package spring.backend.auth.application; +import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import spring.backend.auth.domain.jwt.value.Type; +import spring.backend.member.domain.entity.Member; + +import javax.crypto.SecretKey; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; @Slf4j @Service public class RefreshTokenProviderService { + + private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); + + public static String refreshTokenProvider(Member member) { + // 기한 설정 , 지금부터 2주 + Date exiaryDate = Date.from( + Instant.now().plus(14, ChronoUnit.DAYS) + ); + + // RefreshToken 생성 + return Jwts.builder() + .subject(member.getEmail()) // 추후 변경 가능 + .claim("type", Type.REFRESH.getType()) + .issuedAt(new Date()) + .expiration(exiaryDate) + .signWith(SECRET_KEY) + .compact(); + } } From 78284dd62680347691d1c85d6cb7744fd4432b15 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 01:45:57 +0900 Subject: [PATCH 040/478] =?UTF-8?q?feat:=20(#18)=20LoginResponse=EC=97=90?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=20Role=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/auth/dto/response/LoginResponse.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java index 29642109a..5481466f4 100644 --- a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java +++ b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java @@ -1,4 +1,6 @@ package spring.backend.auth.dto.response; -public record LoginResponse(String accessToken, String refreshToken) { +import spring.backend.member.domain.value.Role; + +public record LoginResponse(String accessToken, String refreshToken, Role role) { } From 79b731b399742167d4a681f785e78e3a1910809c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 01:57:14 +0900 Subject: [PATCH 041/478] =?UTF-8?q?feat:=20(#18)=20SecurityConfig=EB=A5=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfig.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/security/SecurityConfig.java diff --git a/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java b/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java new file mode 100644 index 000000000..dde80a5d6 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java @@ -0,0 +1,25 @@ +package spring.backend.core.configuration.security; + +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.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + http + .csrf((auth) -> auth.disable()) + .formLogin((auth) -> auth.disable()) + .httpBasic((auth) -> auth.disable()); + + http.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()); + + return http.build(); + } +} From 4deeb8216612c6891e6d822dc393b77c18855d08 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 02:12:54 +0900 Subject: [PATCH 042/478] =?UTF-8?q?feat:=20(#18)=20OAuth=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=97=90=20=EC=84=B1=EA=B3=B5=ED=95=9C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20AccessToken,=20RefershToken,=20User?= =?UTF-8?q?=EC=9D=98=20Role=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/HandleOAuthLoginService.java | 9 +++------ .../auth/application/RefreshTokenProviderService.java | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 8fb17effb..277228bda 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -12,6 +12,7 @@ import spring.backend.member.application.CreateMemberWithOAuthService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Provider; +import spring.backend.member.domain.value.Role; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; @Service @@ -41,17 +42,13 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); } - CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() - .provider(provider) - .email(oAuthResourceResponse.getEmail()) - .nickname(oAuthResourceResponse.getName()) - .build(); + CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder().provider(provider).email(oAuthResourceResponse.getEmail()).nickname(oAuthResourceResponse.getName()).build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); /** * todo: 사용자 정보를 가지고 AccessToken, RefreshToken을 생성한다. */ - return null; + return new LoginResponse(AccessTokenProviderService.accessTokenProvider(member), RefreshTokenProviderService.refreshTokenProvider(member), Role.GUEST); } } diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java index ac241ade8..7a6476e55 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java @@ -23,6 +23,8 @@ public static String refreshTokenProvider(Member member) { Instant.now().plus(14, ChronoUnit.DAYS) ); + System.out.println("secret key : " + SECRET_KEY); + // RefreshToken 생성 return Jwts.builder() .subject(member.getEmail()) // 추후 변경 가능 @@ -32,4 +34,4 @@ public static String refreshTokenProvider(Member member) { .signWith(SECRET_KEY) .compact(); } -} +} \ No newline at end of file From 441dd9b27c2bc2e7301092719531606cd833cd5b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 10:36:58 +0900 Subject: [PATCH 043/478] =?UTF-8?q?refactor:=20(#18)=20JWT=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EB=82=B4=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/AccessTokenProviderService.java | 2 -- .../backend/auth/application/RefreshTokenProviderService.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java index b615cb4ec..a5d9dc7b1 100644 --- a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java @@ -17,12 +17,10 @@ public class AccessTokenProviderService { private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); public static String accessTokenProvider(Member member) { - // 기한 설정 , 지금부터 1일 Date expiryDate = Date.from( Instant.now().plus(1, ChronoUnit.DAYS) ); - // AccessToken 생성 return Jwts.builder() .subject(member.getEmail()) // 추후 변경 가능 .claim("type", Type.ACCESS.getType()) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java index 7a6476e55..1f639244b 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java @@ -18,14 +18,12 @@ public class RefreshTokenProviderService { private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); public static String refreshTokenProvider(Member member) { - // 기한 설정 , 지금부터 2주 Date exiaryDate = Date.from( Instant.now().plus(14, ChronoUnit.DAYS) ); System.out.println("secret key : " + SECRET_KEY); - // RefreshToken 생성 return Jwts.builder() .subject(member.getEmail()) // 추후 변경 가능 .claim("type", Type.REFRESH.getType()) From 6a9526ed9c71453bb65ed31215c1a06e5a675b16 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 10:57:35 +0900 Subject: [PATCH 044/478] =?UTF-8?q?fix:=20(#18)=20LoginResponse=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=97=AD=ED=95=A0=EC=9D=84=20member.getRo?= =?UTF-8?q?le()=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 277228bda..88b047a94 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -49,6 +49,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s /** * todo: 사용자 정보를 가지고 AccessToken, RefreshToken을 생성한다. */ - return new LoginResponse(AccessTokenProviderService.accessTokenProvider(member), RefreshTokenProviderService.refreshTokenProvider(member), Role.GUEST); + return new LoginResponse(AccessTokenProviderService.accessTokenProvider(member), RefreshTokenProviderService.refreshTokenProvider(member), member.getRole()); } } From 2b92860686a03549be13d4593ec0cb82d3ab04a9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 13:16:43 +0900 Subject: [PATCH 045/478] =?UTF-8?q?fix:=20(#18)=20=EC=8A=A4=ED=94=84?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20SecurityC?= =?UTF-8?q?onfig=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 --- .../security/SecurityConfig.java | 25 ------------------- 2 files changed, 28 deletions(-) delete mode 100644 src/main/java/spring/backend/core/configuration/security/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 32cdf1446..ba102a1fd 100644 --- a/build.gradle +++ b/build.gradle @@ -53,9 +53,6 @@ dependencies { // Swagger-UI implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - // Security - implementation 'org.springframework.boot:spring-boot-starter-security' - // JWT implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java b/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java deleted file mode 100644 index dde80a5d6..000000000 --- a/src/main/java/spring/backend/core/configuration/security/SecurityConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package spring.backend.core.configuration.security; - -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.web.SecurityFilterChain; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - - http - .csrf((auth) -> auth.disable()) - .formLogin((auth) -> auth.disable()) - .httpBasic((auth) -> auth.disable()); - - http.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()); - - return http.build(); - } -} From 0a4a92dc7fb42f7e45b2948b1d4955677c23d9d4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 13:21:40 +0900 Subject: [PATCH 046/478] =?UTF-8?q?chore:=20(#18)=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 88b047a94..273fdf270 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -46,9 +46,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - /** - * todo: 사용자 정보를 가지고 AccessToken, RefreshToken을 생성한다. - */ return new LoginResponse(AccessTokenProviderService.accessTokenProvider(member), RefreshTokenProviderService.refreshTokenProvider(member), member.getRole()); } } From e0a6e5e6da398456282a1b28958518e259e92579 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 13:22:27 +0900 Subject: [PATCH 047/478] =?UTF-8?q?chore:=20(#18)=20sout=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/RefreshTokenProviderService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java index 1f639244b..eac93691b 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java @@ -22,8 +22,6 @@ public static String refreshTokenProvider(Member member) { Instant.now().plus(14, ChronoUnit.DAYS) ); - System.out.println("secret key : " + SECRET_KEY); - return Jwts.builder() .subject(member.getEmail()) // 추후 변경 가능 .claim("type", Type.REFRESH.getType()) From 4bcc8d05d2f9205e3248bdc436f6c2250d7a4748 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 14:19:01 +0900 Subject: [PATCH 048/478] =?UTF-8?q?fix:=20(#18)=20AccessTokenProviderServi?= =?UTF-8?q?ce=20,=20RefreshTokenProviderService=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccessTokenProviderService.java | 32 ------------------ .../RefreshTokenProviderService.java | 33 ------------------- 2 files changed, 65 deletions(-) delete mode 100644 src/main/java/spring/backend/auth/application/AccessTokenProviderService.java delete mode 100644 src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java diff --git a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java b/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java deleted file mode 100644 index a5d9dc7b1..000000000 --- a/src/main/java/spring/backend/auth/application/AccessTokenProviderService.java +++ /dev/null @@ -1,32 +0,0 @@ -package spring.backend.auth.application; - -import io.jsonwebtoken.Jwts; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import spring.backend.auth.domain.jwt.value.Type; -import spring.backend.member.domain.entity.Member; - -import javax.crypto.SecretKey; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -@Slf4j -@Service -public class AccessTokenProviderService { - private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); - - public static String accessTokenProvider(Member member) { - Date expiryDate = Date.from( - Instant.now().plus(1, ChronoUnit.DAYS) - ); - - return Jwts.builder() - .subject(member.getEmail()) // 추후 변경 가능 - .claim("type", Type.ACCESS.getType()) - .issuedAt(new Date()) - .expiration(expiryDate) - .signWith(SECRET_KEY) - .compact(); - } -} diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java b/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java deleted file mode 100644 index eac93691b..000000000 --- a/src/main/java/spring/backend/auth/application/RefreshTokenProviderService.java +++ /dev/null @@ -1,33 +0,0 @@ -package spring.backend.auth.application; - -import io.jsonwebtoken.Jwts; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import spring.backend.auth.domain.jwt.value.Type; -import spring.backend.member.domain.entity.Member; - -import javax.crypto.SecretKey; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -@Slf4j -@Service -public class RefreshTokenProviderService { - - private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); - - public static String refreshTokenProvider(Member member) { - Date exiaryDate = Date.from( - Instant.now().plus(14, ChronoUnit.DAYS) - ); - - return Jwts.builder() - .subject(member.getEmail()) // 추후 변경 가능 - .claim("type", Type.REFRESH.getType()) - .issuedAt(new Date()) - .expiration(exiaryDate) - .signWith(SECRET_KEY) - .compact(); - } -} \ No newline at end of file From 5df1d80dfb427a6e46186199b31f66b0a8d627d8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 14:19:42 +0900 Subject: [PATCH 049/478] =?UTF-8?q?feat:=20(#18)=20JwtService=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/util/application/JwtService.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/main/java/spring/backend/util/application/JwtService.java diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/util/application/JwtService.java new file mode 100644 index 000000000..dcf92b487 --- /dev/null +++ b/src/main/java/spring/backend/util/application/JwtService.java @@ -0,0 +1,65 @@ +package spring.backend.util.application; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import spring.backend.auth.domain.jwt.value.Type; +import spring.backend.member.domain.entity.Member; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +@Service +public class JwtService { + + private final SecretKey SECRET_KEY; + private final long ACCESS_EXPIRATION; + private final long REFRESH_EXPIRATION; + + public JwtService( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-token-expiry}") long accessTokenExpiry, + @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry + ) { + this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); // 문자열을 SecretKey로 변환 + this.ACCESS_EXPIRATION = accessTokenExpiry; // 초 단위 만료 시간 + this.REFRESH_EXPIRATION = refreshTokenExpiry; + } + + + public String provideAccessToken(Member member) { + return provideToken(member.getEmail(), member.getId(), Type.ACCESS, ACCESS_EXPIRATION); + } + + + public String provideRefreshToken(Member member) { + return provideToken(member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION); + } + + private String provideToken(String email, UUID id, Type type, long expiration) { + Date exiaryDate = Date.from( + Instant.now().plus(expiration, ChronoUnit.HOURS) + ); + + return Jwts.builder() + .subject(email) + .claims( + Map.of( + "id", id.toString(), + "type", type.getType() + ) + ) + .issuedAt(new Date()) + .expiration(exiaryDate) + .signWith(SECRET_KEY) + .compact(); + + } +} From f6b82ad79f21e1f347639f34bf95fda7ac84aef6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 15:00:00 +0900 Subject: [PATCH 050/478] =?UTF-8?q?chore:=20(#18)=20JwtService=20=EC=86=8D?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/util/application/JwtService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/util/application/JwtService.java index dcf92b487..2fb53a81b 100644 --- a/src/main/java/spring/backend/util/application/JwtService.java +++ b/src/main/java/spring/backend/util/application/JwtService.java @@ -28,8 +28,8 @@ public JwtService( @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry ) { - this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); // 문자열을 SecretKey로 변환 - this.ACCESS_EXPIRATION = accessTokenExpiry; // 초 단위 만료 시간 + this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.ACCESS_EXPIRATION = accessTokenExpiry; this.REFRESH_EXPIRATION = refreshTokenExpiry; } @@ -43,11 +43,10 @@ public String provideRefreshToken(Member member) { return provideToken(member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION); } - private String provideToken(String email, UUID id, Type type, long expiration) { + private String provideToken(String email, UUID id,Type type, long expiration) { Date exiaryDate = Date.from( Instant.now().plus(expiration, ChronoUnit.HOURS) ); - return Jwts.builder() .subject(email) .claims( From 7eb9e0e5e6b7429c30ad6fbcb1252f5d9e9cfa9e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 15:01:29 +0900 Subject: [PATCH 051/478] =?UTF-8?q?chore:=20(#18)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/infrastructure/jpa/shared/BaseEntity.java | 5 ++++- .../java/spring/backend/util/application/JwtService.java | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java index 1cf61e76f..54ca96bc4 100644 --- a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java @@ -1,7 +1,10 @@ package spring.backend.core.infrastructure.jpa.shared; import jakarta.persistence.*; -import lombok.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/util/application/JwtService.java index 2fb53a81b..15c5be559 100644 --- a/src/main/java/spring/backend/util/application/JwtService.java +++ b/src/main/java/spring/backend/util/application/JwtService.java @@ -3,7 +3,6 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import spring.backend.auth.domain.jwt.value.Type; import spring.backend.member.domain.entity.Member; From d1ac344387a9cbf62517017327b867ddd2cd0766 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 15:02:20 +0900 Subject: [PATCH 052/478] =?UTF-8?q?fix:=20(#18)=20jwtService=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 273fdf270..232987b80 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -12,8 +12,8 @@ import spring.backend.member.application.CreateMemberWithOAuthService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Provider; -import spring.backend.member.domain.value.Role; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; +import spring.backend.util.application.JwtService; @Service @RequiredArgsConstructor @@ -24,6 +24,8 @@ public class HandleOAuthLoginService { private final CreateMemberWithOAuthService createMemberWithOAuthService; + private final JwtService jwtService; + public LoginResponse handleOAuthLogin(String providerName, String code, String state) { if (providerName == null || providerName.isEmpty()) { throw AuthenticationErrorCode.NOT_EXIST_PROVIDER.toException(); @@ -46,6 +48,7 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - return new LoginResponse(AccessTokenProviderService.accessTokenProvider(member), RefreshTokenProviderService.refreshTokenProvider(member), member.getRole()); + return new LoginResponse(jwtService.provideAccessToken(member), jwtService.provideRefreshToken(member), member.getRole()); + } } From 0aa1adc9721805a11d9a53f830e431aaa158df04 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 15:03:56 +0900 Subject: [PATCH 053/478] =?UTF-8?q?fix:=20(#18)=20=EA=B0=80=EC=9E=85?= =?UTF-8?q?=ED=95=9C=20=EB=A9=A4=EB=B2=84=20=EB=B0=98=ED=99=98=20=EC=8B=9C?= =?UTF-8?q?=20entity=EC=9D=98=20=EA=B0=92=EC=9D=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20-=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=A0=20=EC=A0=80=EC=9E=A5=EB=90=9C=20entity?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9C=BC=EB=AF=80=EB=A1=9C=20id=EB=A5=BC=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=AC=20=EC=88=98=20=EC=97=86=EC=97=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMemberWithOAuthService.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java index 0adb15923..012fa0b6c 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -26,7 +26,14 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { if (members == null || members.isEmpty()) { Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); memberRepository.save(newMember); - return newMember; + Member savedMember = memberRepository.findByEmail(request.getEmail()); + + if (savedMember == null) { + log.error("[CreateMemberWithOAuthService] member could not be saved"); + throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); + } + + return savedMember; } Member existingMember = members.stream() .filter(Member::isMember) @@ -45,7 +52,15 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { .orElseGet(() -> { Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); memberRepository.save(newMember); - return newMember; + + Member savedMember = memberRepository.findByEmail(request.getEmail()); + + if (savedMember == null) { + log.error("[CreateMemberWithOAuthService] member could not be saved"); + throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); + } + + return savedMember; }); } } From b9cf4503f01b9b6773fda27cecd073a255e9dcd6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 20:06:33 +0900 Subject: [PATCH 054/478] =?UTF-8?q?fix:=20(#18)=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=8B=9C=20subject=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B3=A0=20claims=EC=97=90=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/spring/backend/util/application/JwtService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/util/application/JwtService.java index 15c5be559..cb26ecc24 100644 --- a/src/main/java/spring/backend/util/application/JwtService.java +++ b/src/main/java/spring/backend/util/application/JwtService.java @@ -47,10 +47,10 @@ private String provideToken(String email, UUID id,Type type, long expiration) { Instant.now().plus(expiration, ChronoUnit.HOURS) ); return Jwts.builder() - .subject(email) .claims( Map.of( - "id", id.toString(), + "memberId", id.toString(), + "email", email, "type", type.getType() ) ) From d5c42131a931c2ee7af625da98cae78b06f32620 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 20:11:33 +0900 Subject: [PATCH 055/478] =?UTF-8?q?chore:=20(#18)=20=ED=86=A0=ED=81=B0=20T?= =?UTF-8?q?ype=20Enum=EC=9D=84=20JwtService=20=EC=95=88=EC=97=90=20?= =?UTF-8?q?=EB=84=A3=EB=8A=94=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/domain/jwt/value/Type.java | 13 ------------ .../backend/util/application/JwtService.java | 20 +++++++++++++------ 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 src/main/java/spring/backend/auth/domain/jwt/value/Type.java diff --git a/src/main/java/spring/backend/auth/domain/jwt/value/Type.java b/src/main/java/spring/backend/auth/domain/jwt/value/Type.java deleted file mode 100644 index 3268c2e52..000000000 --- a/src/main/java/spring/backend/auth/domain/jwt/value/Type.java +++ /dev/null @@ -1,13 +0,0 @@ -package spring.backend.auth.domain.jwt.value; - -import lombok.Getter; - -@Getter -public enum Type { - ACCESS("access"), REFRESH("refresh"); - private final String type; - - Type(String type) { - this.type = type; - } -} diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/util/application/JwtService.java index cb26ecc24..52649d14d 100644 --- a/src/main/java/spring/backend/util/application/JwtService.java +++ b/src/main/java/spring/backend/util/application/JwtService.java @@ -2,9 +2,10 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import spring.backend.auth.domain.jwt.value.Type; import spring.backend.member.domain.entity.Member; import javax.crypto.SecretKey; @@ -15,9 +16,17 @@ import java.util.Map; import java.util.UUID; + @Service public class JwtService { + @Getter + @RequiredArgsConstructor + public enum Type { + ACCESS("access"), REFRESH("refresh"); + private final String type; + } + private final SecretKey SECRET_KEY; private final long ACCESS_EXPIRATION; private final long REFRESH_EXPIRATION; @@ -42,9 +51,9 @@ public String provideRefreshToken(Member member) { return provideToken(member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION); } - private String provideToken(String email, UUID id,Type type, long expiration) { - Date exiaryDate = Date.from( - Instant.now().plus(expiration, ChronoUnit.HOURS) + private String provideToken(String email, UUID id, Type type, long expiration) { + Date expiryDate = Date.from( + Instant.now().plus(expiration, ChronoUnit.DAYS) ); return Jwts.builder() .claims( @@ -55,9 +64,8 @@ private String provideToken(String email, UUID id,Type type, long expiration) { ) ) .issuedAt(new Date()) - .expiration(exiaryDate) + .expiration(expiryDate) .signWith(SECRET_KEY) .compact(); - } } From 32c278a7cbc181312887f9b24cdaf594a6bc28d2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 20:17:19 +0900 Subject: [PATCH 056/478] =?UTF-8?q?refactor:=20(#18)=20util=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A5=BC=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=20core/a?= =?UTF-8?q?pplication=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=B4=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=ED=8F=B4=EB=8D=94=20=EC=95=88=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?JwtService=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 2 +- .../application/JwtService.java | 27 +++---------------- 2 files changed, 5 insertions(+), 24 deletions(-) rename src/main/java/spring/backend/{util => core}/application/JwtService.java (63%) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 232987b80..2f3ec8f5d 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -13,7 +13,7 @@ import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Provider; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; -import spring.backend.util.application.JwtService; +import spring.backend.core.application.JwtService; @Service @RequiredArgsConstructor diff --git a/src/main/java/spring/backend/util/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java similarity index 63% rename from src/main/java/spring/backend/util/application/JwtService.java rename to src/main/java/spring/backend/core/application/JwtService.java index 52649d14d..b9939ebf4 100644 --- a/src/main/java/spring/backend/util/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -1,4 +1,4 @@ -package spring.backend.util.application; +package spring.backend.core.application; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -31,41 +31,22 @@ public enum Type { private final long ACCESS_EXPIRATION; private final long REFRESH_EXPIRATION; - public JwtService( - @Value("${jwt.secret}") String secret, - @Value("${jwt.access-token-expiry}") long accessTokenExpiry, - @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry - ) { + public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry) { this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.ACCESS_EXPIRATION = accessTokenExpiry; this.REFRESH_EXPIRATION = refreshTokenExpiry; } - public String provideAccessToken(Member member) { return provideToken(member.getEmail(), member.getId(), Type.ACCESS, ACCESS_EXPIRATION); } - public String provideRefreshToken(Member member) { return provideToken(member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION); } private String provideToken(String email, UUID id, Type type, long expiration) { - Date expiryDate = Date.from( - Instant.now().plus(expiration, ChronoUnit.DAYS) - ); - return Jwts.builder() - .claims( - Map.of( - "memberId", id.toString(), - "email", email, - "type", type.getType() - ) - ) - .issuedAt(new Date()) - .expiration(expiryDate) - .signWith(SECRET_KEY) - .compact(); + Date expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); + return Jwts.builder().claims(Map.of("memberId", id.toString(), "email", email, "type", type.getType())).issuedAt(new Date()).expiration(expiryDate).signWith(SECRET_KEY).compact(); } } From e6ea42df694b474e30385b4b68f254f6963af434 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Oct 2024 22:37:26 +0900 Subject: [PATCH 057/478] =?UTF-8?q?fix:=20(#18)=20memberRepository?= =?UTF-8?q?=EC=9D=98=20save()=20=EB=A9=94=EC=84=9C=EB=93=9C=EA=B0=80=20Mem?= =?UTF-8?q?ber=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/repository/MemberRepository.java | 2 +- .../persistence/jpa/adapter/MemberRepositoryImpl.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java index bbb0d9f45..14ffec7ef 100644 --- a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java +++ b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java @@ -8,7 +8,7 @@ public interface MemberRepository { Member findById(UUID id); - void save(Member member); + Member save(Member member); Member findByEmail(String email); List findAllByEmail(String email); } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java index 5c6692c72..8d03339fd 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java @@ -32,12 +32,13 @@ public Member findById(UUID id) { } @Override - public void save(Member member) { + public Member save(Member member) { MemberJpaEntity memberJpaEntity = memberMapper.toJpaEntity(member); if (memberJpaEntity == null) { throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); } memberJpaRepository.save(memberJpaEntity); + return memberMapper.toDomainEntity(memberJpaEntity); } @Override From ca08f952a1a393ae18542081295b82e04d1c3492 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Oct 2024 00:15:55 +0900 Subject: [PATCH 058/478] =?UTF-8?q?refactor:=20(#18)=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EB=B0=94=EB=80=90=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9B=90=EB=B3=B5=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/infrastructure/jpa/shared/BaseEntity.java | 5 +---- .../member/application/CreateMemberWithOAuthService.java | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java index 54ca96bc4..1cf61e76f 100644 --- a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java @@ -1,10 +1,7 @@ package spring.backend.core.infrastructure.jpa.shared; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java index 012fa0b6c..5e46a2310 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -25,8 +25,7 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { List members = memberRepository.findAllByEmail(request.getEmail()); if (members == null || members.isEmpty()) { Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); - memberRepository.save(newMember); - Member savedMember = memberRepository.findByEmail(request.getEmail()); + Member savedMember = memberRepository.save(newMember); if (savedMember == null) { log.error("[CreateMemberWithOAuthService] member could not be saved"); @@ -51,9 +50,7 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { .findFirst() .orElseGet(() -> { Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); - memberRepository.save(newMember); - - Member savedMember = memberRepository.findByEmail(request.getEmail()); + Member savedMember = memberRepository.save(newMember); if (savedMember == null) { log.error("[CreateMemberWithOAuthService] member could not be saved"); From 60c76ec29ba896168e2c6bbcfce1bb126cc4102b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Oct 2024 01:24:29 +0900 Subject: [PATCH 059/478] =?UTF-8?q?refactor:=20(#18)=20JwtService=EC=9D=98?= =?UTF-8?q?=20=EB=A6=AC=ED=84=B4=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 6 ++++- .../backend/core/application/JwtService.java | 24 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 2f3ec8f5d..41cede529 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -44,7 +44,11 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); } - CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder().provider(provider).email(oAuthResourceResponse.getEmail()).nickname(oAuthResourceResponse.getName()).build(); + CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() + .provider(provider) + .email(oAuthResourceResponse.getEmail()) + .nickname(oAuthResourceResponse.getName()) + .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index b9939ebf4..f671a7986 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -38,15 +38,33 @@ public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-to } public String provideAccessToken(Member member) { - return provideToken(member.getEmail(), member.getId(), Type.ACCESS, ACCESS_EXPIRATION); + return provideToken( + member.getEmail(), + member.getId(), + Type.ACCESS, + ACCESS_EXPIRATION + ); } public String provideRefreshToken(Member member) { - return provideToken(member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION); + return provideToken( + member.getEmail(), + member.getId(), + Type.REFRESH, + REFRESH_EXPIRATION + ); } private String provideToken(String email, UUID id, Type type, long expiration) { Date expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); - return Jwts.builder().claims(Map.of("memberId", id.toString(), "email", email, "type", type.getType())).issuedAt(new Date()).expiration(expiryDate).signWith(SECRET_KEY).compact(); + return Jwts.builder() + .claims(Map.of( + "memberId", id.toString(), + "email", email, + "type", type.getType())) + .issuedAt(new Date()) + .expiration(expiryDate) + .signWith(SECRET_KEY) + .compact(); } } From 549886a2ae12067a263ac9fb6c35c2652e711c8d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Oct 2024 12:53:22 +0900 Subject: [PATCH 060/478] =?UTF-8?q?chore:=20(#20)=20=EB=AA=A8=EB=8B=88?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=84=9C=EB=B2=84=20=EA=B5=AC=EC=B6=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20gradle=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index ba102a1fd..c4c260d88 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,10 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // monitoring (Prometheus) + implementation 'org.springframework.boot:spring-boot-starter-actuator' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' } tasks.named('test') { From 7097315f991fef71baab1deecc446daf6a569bb5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Oct 2024 17:59:05 +0900 Subject: [PATCH 061/478] =?UTF-8?q?chore:=20(#22)=20=EB=8F=84=EC=BB=A4?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index c259f94cc..fbd028c06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,8 @@ -FROM gradle:8.10.1-jdk17 AS build - -WORKDIR /app - -COPY . /app - -RUN gradle clean build --no-daemon - FROM openjdk:17.0.1-jdk-slim WORKDIR /app -COPY --from=build /app/build/libs/*.jar /app/backend.jar +COPY ./build/libs/backend-0.0.1-SNAPSHOT.jar /app/backend.jar EXPOSE 8080 ENTRYPOINT ["java"] From 2d7972c67a0531b899d1e81e8aee898a72f24a1a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Oct 2024 23:03:16 +0900 Subject: [PATCH 062/478] =?UTF-8?q?chore:=20(#22)=20.dockerignore=EC=97=90?= =?UTF-8?q?=EC=84=9C=20build=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 4ca6a34f2..f29fa3f60 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ .git/ .gitignore/ .idea/ -build/ \ No newline at end of file From 77914fe70beee1c1bf51cb3dcdea2974bb3638f3 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 16 Oct 2024 23:25:21 +0900 Subject: [PATCH 063/478] =?UTF-8?q?fix:=20(#24)=20AuthenticationException?= =?UTF-8?q?=EC=9D=84=20DomainException=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/exception/AuthenticationErrorCode.java | 9 +++++---- .../backend/core/exception/AuthenticationException.java | 8 -------- 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 src/main/java/spring/backend/core/exception/AuthenticationException.java diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 534bb3ab2..08003b3c5 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -3,17 +3,18 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import spring.backend.core.exception.AuthenticationException; +import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.BaseErrorCode; @Getter @RequiredArgsConstructor -public enum AuthenticationErrorCode implements BaseErrorCode { +public enum AuthenticationErrorCode implements BaseErrorCode { NOT_EXIST_HEADER(HttpStatus.UNAUTHORIZED, "Authorization Header가 존재하지 않습니다."), NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "Authorization Header에 Token이 존재하지 않습니다."), NOT_MATCH_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "토큰의 형식이 맞지 않습니다."), NOT_DEFINE_TOKEN(HttpStatus.UNAUTHORIZED, "정의되지 않은 토큰입니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), INVALID_PROVIDER(HttpStatus.BAD_REQUEST, "유효한 OAuth 써드파티 제공자가 아닙니다."), NOT_EXIST_PROVIDER(HttpStatus.BAD_REQUEST, "OAuth 써드파티 제공자가 존재하지 않습니다."), NOT_EXIST_AUTH_CODE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 제공받은 인증 코드가 존재하지 않습니다."), @@ -26,7 +27,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode Date: Thu, 17 Oct 2024 00:21:15 +0900 Subject: [PATCH 064/478] =?UTF-8?q?chore:=20(#24)=20test=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=9D=98=20property=EC=9D=98=20profiles=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/spring/backend/BackendApplicationTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/spring/backend/BackendApplicationTests.java b/src/test/java/spring/backend/BackendApplicationTests.java index 841f0d44d..6079af5b6 100644 --- a/src/test/java/spring/backend/BackendApplicationTests.java +++ b/src/test/java/spring/backend/BackendApplicationTests.java @@ -2,10 +2,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; @SpringBootTest -@ActiveProfiles("local") class BackendApplicationTests { @Test From cb03c2f808198beb404d28b8eedca80263f360eb Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 17 Oct 2024 00:22:31 +0900 Subject: [PATCH 065/478] =?UTF-8?q?feat:=20(#24)=20jwt=EC=9D=98=20payload?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EC=A0=B8=EC=98=AC=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/AuthenticationErrorCode.java | 1 + .../backend/core/application/JwtService.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 08003b3c5..add6cb6bc 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -13,6 +13,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { NOT_EXIST_HEADER(HttpStatus.UNAUTHORIZED, "Authorization Header가 존재하지 않습니다."), NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "Authorization Header에 Token이 존재하지 않습니다."), NOT_MATCH_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "토큰의 형식이 맞지 않습니다."), + INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED, "토큰의 서명이 올바르지 않습니다."), NOT_DEFINE_TOKEN(HttpStatus.UNAUTHORIZED, "정의되지 않은 토큰입니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), INVALID_PROVIDER(HttpStatus.BAD_REQUEST, "유효한 OAuth 써드파티 제공자가 아닙니다."), diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index f671a7986..7765d4bf2 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -1,11 +1,16 @@ package spring.backend.core.application; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.member.domain.entity.Member; import javax.crypto.SecretKey; @@ -55,6 +60,24 @@ public String provideRefreshToken(Member member) { ); } + public Claims getPayload(String token) { + try { + return Jwts.parser() + .verifyWith(SECRET_KEY) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (SignatureException e) { + throw AuthenticationErrorCode.INVALID_SIGNATURE.toException(); + } catch (ExpiredJwtException e) { + throw AuthenticationErrorCode.EXPIRED_TOKEN.toException(); + } catch (MalformedJwtException e) { + throw AuthenticationErrorCode.NOT_MATCH_TOKEN_FORMAT.toException(); + } catch (Exception e) { + throw AuthenticationErrorCode.NOT_DEFINE_TOKEN.toException(); + } + } + private String provideToken(String email, UUID id, Type type, long expiration) { Date expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); return Jwts.builder() From 1bc209e82539afe203996382d04418eb870537c4 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 17 Oct 2024 00:23:22 +0900 Subject: [PATCH 066/478] =?UTF-8?q?feat:=20(#24)=20jwt=EC=9D=98=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EA=B8=B0=EA=B0=84=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/core/application/JwtService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index 7765d4bf2..fa91f2a9a 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -78,6 +78,13 @@ public Claims getPayload(String token) { } } + public void validateTokenExpiration(String token) { + Claims claims = getPayload(token); + if (claims.getExpiration().before(new Date())) { + throw AuthenticationErrorCode.EXPIRED_TOKEN.toException(); + } + } + private String provideToken(String email, UUID id, Type type, long expiration) { Date expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); return Jwts.builder() From f3d029d562f425a9c915e18491797bf802e545fc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 17 Oct 2024 00:25:06 +0900 Subject: [PATCH 067/478] =?UTF-8?q?feat:=20(#24)=20jwt=20getPayload(),=20v?= =?UTF-8?q?alidateTokenExpiration()=20=EB=A9=94=EC=86=8C=EB=93=9C=EC=9D=98?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/JwtServiceTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/test/java/spring/backend/core/application/JwtServiceTest.java diff --git a/src/test/java/spring/backend/core/application/JwtServiceTest.java b/src/test/java/spring/backend/core/application/JwtServiceTest.java new file mode 100644 index 000000000..060fc2b00 --- /dev/null +++ b/src/test/java/spring/backend/core/application/JwtServiceTest.java @@ -0,0 +1,77 @@ +package spring.backend.core.application; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.exception.DomainException; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class JwtServiceTest { + + @Autowired + private JwtService jwtService; + + @Value("${jwt.secret}") + private String secret; + private String validJwt; + private String invalidJwt; + private String expiredJwt; + + @BeforeEach + void setUp() { + long expirationTime = 1; + Date expiryDate = Date.from(Instant.now().plus(expirationTime, ChronoUnit.DAYS)); + SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + + validJwt = Jwts.builder() + .claim("name", "test") + .issuedAt(new Date()) + .expiration(expiryDate) + .signWith(secretKey) + .compact(); + + invalidJwt = validJwt + "invalid"; + + expiryDate = Date.from(Instant.now().minus(expirationTime, ChronoUnit.DAYS)); + expiredJwt = Jwts.builder() + .claim("name", "test") + .issuedAt(new Date()) + .expiration(expiryDate) + .signWith(secretKey) + .compact(); + } + + @DisplayName("유효한 JWT를 파싱하여 claim을 반환한다") + @Test + void getPayloadWithValidJwt() { + // when + Claims claims = jwtService.getPayload(validJwt); + + // then + assertThat(claims.get("name")).isEqualTo("test"); + } + + @DisplayName("만료된 토큰일 경우 예외를 발생시킨다") + @Test + void validateTokenExpirationWithExpiredJwt() { + // when & then + DomainException ex = assertThrows(DomainException.class, () -> jwtService.validateTokenExpiration(expiredJwt), "만료된 토큰입니다."); + assertThat(ex.getCode()).isEqualTo(AuthenticationErrorCode.EXPIRED_TOKEN.name()); + } +} \ No newline at end of file From 4368626189efd59f9cdd61eea11525340d287f7f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 17 Oct 2024 00:25:38 +0900 Subject: [PATCH 068/478] =?UTF-8?q?feat:=20(#24)=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=84=B0=EC=97=90=EC=84=9C=20Authorization=EB=A5=BC?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interceptor/Authorization.java | 11 ++++ .../interceptor/AuthorizationInterceptor.java | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/interceptor/Authorization.java create mode 100644 src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java diff --git a/src/main/java/spring/backend/core/configuration/interceptor/Authorization.java b/src/main/java/spring/backend/core/configuration/interceptor/Authorization.java new file mode 100644 index 000000000..1fc418e04 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/interceptor/Authorization.java @@ -0,0 +1,11 @@ +package spring.backend.core.configuration.interceptor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authorization { +} diff --git a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java new file mode 100644 index 000000000..4a6a174bf --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java @@ -0,0 +1,52 @@ +package spring.backend.core.configuration.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.application.JwtService; + +import java.lang.annotation.Annotation; + +@Component +@RequiredArgsConstructor +public class AuthorizationInterceptor implements HandlerInterceptor { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer"; + + private final JwtService jwtService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (HttpMethod.OPTIONS.name().equals(request.getMethod())) { + return true; + } + String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Annotation authorizationAnnotation = handlerMethod.getMethodAnnotation(Authorization.class); + if (authorizationAnnotation != null) { + String token = extractToken(authorizationHeader); + jwtService.validateTokenExpiration(token); + } + } + return true; + } + + private String extractToken(String authorizationHeader) { + if (authorizationHeader == null) { + throw AuthenticationErrorCode.NOT_EXIST_HEADER.toException(); + } + try { + return authorizationHeader.split(AUTHORIZATION_BEARER_PREFIX)[1].replace(" ", ""); + } catch (Exception e) { + throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); + } + } +} From 0a5ec9d8213f8945ae8577886a6c60f6025b7367 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 17 Oct 2024 00:26:11 +0900 Subject: [PATCH 069/478] =?UTF-8?q?feat:=20(#24)=20Authorization=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/WebMvcConfiguration.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java new file mode 100644 index 000000000..8c1a34048 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -0,0 +1,19 @@ +package spring.backend.core.configuration; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import spring.backend.core.configuration.interceptor.AuthorizationInterceptor; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfiguration implements WebMvcConfigurer { + + private final AuthorizationInterceptor authorizationInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authorizationInterceptor); + } +} From 5befb762e70520df9947c788189a4ae114f25b13 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 19 Oct 2024 14:41:56 +0900 Subject: [PATCH 070/478] =?UTF-8?q?feat:=20(#32)=20ArgumentResolver?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=94=EC=9D=B8=EB=94=A9=ED=95=A0=20?= =?UTF-8?q?=EB=95=8C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20LoginMember?= =?UTF-8?q?=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/argumentresolver/LoginMember.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/LoginMember.java diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMember.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMember.java new file mode 100644 index 000000000..4b180cc29 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMember.java @@ -0,0 +1,11 @@ +package spring.backend.core.configuration.argumentresolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginMember { +} From da81d15ea55a88ed841d8dc33aec891709d8540f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 19 Oct 2024 14:43:19 +0900 Subject: [PATCH 071/478] =?UTF-8?q?feat:=20(#32)=20ArgumentResolver?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=B4=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=EB=A9=A4=EB=B2=84=EB=A1=9C=20=EB=B0=94=EC=9D=B8?= =?UTF-8?q?=EB=94=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/WebMvcConfiguration.java | 11 ++++ .../LoginMemberArgumentResolver.java | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index 8c1a34048..2c2d990c1 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -2,18 +2,29 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import spring.backend.core.configuration.argumentresolver.LoginMemberArgumentResolver; import spring.backend.core.configuration.interceptor.AuthorizationInterceptor; +import java.util.List; + @Configuration @RequiredArgsConstructor public class WebMvcConfiguration implements WebMvcConfigurer { private final AuthorizationInterceptor authorizationInterceptor; + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } } diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..729633f62 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java @@ -0,0 +1,53 @@ +package spring.backend.core.configuration.argumentresolver; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.application.JwtService; +import spring.backend.member.application.MemberService; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer"; + + private final JwtService jwtService; + + private final MemberService memberService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String authorizationHeader = webRequest.getHeader(AUTHORIZATION_HEADER); + String token = extractToken(authorizationHeader); + UUID memberId = jwtService.extractMemberId(token); + return memberService.findByMemberId(memberId); + } + + private String extractToken(String authorizationHeader) { + if (authorizationHeader == null) { + throw AuthenticationErrorCode.NOT_EXIST_HEADER.toException(); + } + try { + return authorizationHeader.split(AUTHORIZATION_BEARER_PREFIX)[1].replace(" ", ""); + } catch (Exception e) { + throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); + } + } +} From 2cba481a3ab15bb2eb6e5a77d02e6bbbb956fd94 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 19 Oct 2024 14:44:42 +0900 Subject: [PATCH 072/478] =?UTF-8?q?feat:=20(#32)=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A9=A4=EB=B2=84=20id=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/core/application/JwtService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index fa91f2a9a..c4ff710e0 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -78,6 +78,11 @@ public Claims getPayload(String token) { } } + public UUID extractMemberId(String token) { + Claims claims = getPayload(token); + return UUID.fromString(claims.get("memberId", String.class)); + } + public void validateTokenExpiration(String token) { Claims claims = getPayload(token); if (claims.getExpiration().before(new Date())) { From 7953261994e6b79e4c705dc56debd839c32f2d23 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 19 Oct 2024 14:45:40 +0900 Subject: [PATCH 073/478] =?UTF-8?q?feat:=20(#32)=20=EB=A9=A4=EB=B2=84=20id?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberService.java | 22 +++++++++++++++++++ .../member/exception/MemberErrorCode.java | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/member/application/MemberService.java diff --git a/src/main/java/spring/backend/member/application/MemberService.java b/src/main/java/spring/backend/member/application/MemberService.java new file mode 100644 index 000000000..7abe62136 --- /dev/null +++ b/src/main/java/spring/backend/member/application/MemberService.java @@ -0,0 +1,22 @@ +package spring.backend.member.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.exception.MemberErrorCode; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + public Member findByMemberId(UUID memberId) { + Member member = memberRepository.findById(memberId); + return Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException); + } +} diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index b4065ea25..6348f47a6 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -12,7 +12,8 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_EXIST_CONDITION(HttpStatus.BAD_REQUEST, "요청 조건이 존재하지 않습니다."), ALREADY_REGISTERED_WITH_DIFFERENT_OAUTH2(HttpStatus.BAD_REQUEST, "이미 다른 소셜 로그인으로 가입된 계정입니다."), - MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."); + MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."), + NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."); private final HttpStatus httpStatus; From f3479531651979e2fd6f1edae94787c5fb8dfd42 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 19 Oct 2024 14:52:17 +0900 Subject: [PATCH 074/478] =?UTF-8?q?feat:=20(#32)=20LoginMemberArgumentReso?= =?UTF-8?q?lver=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoginMemberArgumentResolverTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java new file mode 100644 index 000000000..d03a5bc79 --- /dev/null +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java @@ -0,0 +1,79 @@ +package spring.backend.core.configuration.argumentresolver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import spring.backend.core.application.JwtService; +import spring.backend.member.application.MemberService; +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LoginMemberArgumentResolverTest { + + @InjectMocks + private LoginMemberArgumentResolver loginMemberArgumentResolver; + + @Mock + private JwtService jwtService; + + @Mock + private MemberService memberService; + + @Mock + private NativeWebRequest webRequest; + + @Mock + private ModelAndViewContainer mavContainer; + + private UUID memberId; + private String token; + private Member member; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + memberId = UUID.randomUUID(); + member = Member.builder() + .id(memberId) + .build(); + token = jwtService.provideAccessToken(member); + } + + @DisplayName("LoginMember 어노테이션이 있는 경우 지원한다") + @Test + public void supportsParameterReturnsTrueForLoginMember() { + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); + Assertions.assertTrue(loginMemberArgumentResolver.supportsParameter(parameter)); + } + + @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 Member 객체를 반환한다") + @Test + public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Exception { + // when + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); + when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); + when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); + when(memberService.findByMemberId(memberId)).thenReturn(member); + + // then + Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); + assertNotNull(result); + assertThat(result).isEqualTo(member); + } +} From 3d65873cc4680a7a3c8b97e5eecb116325123e8c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 12:39:56 +0900 Subject: [PATCH 075/478] =?UTF-8?q?chore:=20(#28)=20Redis=20Configuration?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/redis/RedisConfiguration.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java new file mode 100644 index 000000000..32591f031 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java @@ -0,0 +1,7 @@ +package spring.backend.core.configuration.redis; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedisConfiguration { +} From f892a50c7b64c8e4b27862efa4dac12df789d8ac Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 12:41:15 +0900 Subject: [PATCH 076/478] =?UTF-8?q?chore:=20(#28)=20Redis=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index c4c260d88..071cc6866 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,9 @@ dependencies { // monitoring (Prometheus) implementation 'org.springframework.boot:spring-boot-starter-actuator' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { From a04cd51871a5f3422ee8bdeb18f0aba10268ebdf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 12:50:19 +0900 Subject: [PATCH 077/478] =?UTF-8?q?chore:=20(#28)=20RedisConfiguration?= =?UTF-8?q?=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/redis/RedisConfiguration.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java index 32591f031..5e47ba618 100644 --- a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java @@ -1,7 +1,21 @@ package spring.backend.core.configuration.redis; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; @Configuration public class RedisConfiguration { + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); + } } From 0a805dfa6a7cd0b7efad74d9a255e4dffc109145 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 19:14:16 +0900 Subject: [PATCH 078/478] =?UTF-8?q?feat:=20(#28)=20Redis=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/RedisService.java | 25 ++++++++++++++++ .../redis/RedisConfiguration.java | 9 ++++++ .../redis/repository/RedisRepository.java | 30 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/main/java/spring/backend/core/application/RedisService.java create mode 100644 src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java diff --git a/src/main/java/spring/backend/core/application/RedisService.java b/src/main/java/spring/backend/core/application/RedisService.java new file mode 100644 index 000000000..d660af5ec --- /dev/null +++ b/src/main/java/spring/backend/core/application/RedisService.java @@ -0,0 +1,25 @@ +package spring.backend.core.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import spring.backend.core.infrastructure.redis.repository.RedisRepository; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class RedisService { + private final RedisRepository redisRepository; + + public void save(String key, String value) { + redisRepository.save(key, value); + } + + public void save(String key, String value, Long expireTime, TimeUnit timeUnit) { + redisRepository.save(key, value, expireTime, timeUnit); + } + + public String get(String key) { + return redisRepository.get(key); + } +} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java index 5e47ba618..58a7fb009 100644 --- a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; @Configuration public class RedisConfiguration { @@ -18,4 +19,12 @@ public class RedisConfiguration { public LettuceConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); } + + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } } diff --git a/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java b/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java new file mode 100644 index 000000000..b13a11f73 --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java @@ -0,0 +1,30 @@ +package spring.backend.core.infrastructure.redis.repository; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Repository; + +import java.util.concurrent.TimeUnit; + +@Repository +public class RedisRepository { + private final RedisTemplate redisTemplate; + + public RedisRepository(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + public void save(String key, String value) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + public void save(String key, String value, Long expireTime, TimeUnit timeUnit) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value, expireTime, timeUnit); + } + + public String get(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(key); + } +} From 7d3ca97f6302927be878984bc371780730335f42 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 20:28:26 +0900 Subject: [PATCH 079/478] =?UTF-8?q?feat:=20(#28)=20RefreshToken=EC=9D=84?= =?UTF-8?q?=20Redis=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/RedisService.java | 34 +++++++++++++++---- .../redis/repository/RedisRepository.java | 15 ++++---- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/main/java/spring/backend/core/application/RedisService.java b/src/main/java/spring/backend/core/application/RedisService.java index d660af5ec..e023efff7 100644 --- a/src/main/java/spring/backend/core/application/RedisService.java +++ b/src/main/java/spring/backend/core/application/RedisService.java @@ -4,6 +4,8 @@ import org.springframework.stereotype.Service; import spring.backend.core.infrastructure.redis.repository.RedisRepository; +import java.time.temporal.ChronoUnit; +import java.util.UUID; import java.util.concurrent.TimeUnit; @Service @@ -11,15 +13,33 @@ public class RedisService { private final RedisRepository redisRepository; - public void save(String key, String value) { - redisRepository.save(key, value); + public void saveRefreshToken(UUID memberId, String refreshToken, Long expireTime, ChronoUnit chronoUnit) { + TimeUnit timeUnit = convertChronoUnitToTimeUnit(chronoUnit); + redisRepository.saveRefreshToken(memberId, refreshToken, expireTime, timeUnit); } - public void save(String key, String value, Long expireTime, TimeUnit timeUnit) { - redisRepository.save(key, value, expireTime, timeUnit); + public String getRefreshToken(UUID memberId) { + return redisRepository.getRefreshToken(memberId); } - public String get(String key) { - return redisRepository.get(key); - } + private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { + switch (chronoUnit) { + case NANOS: + return TimeUnit.NANOSECONDS; + case MICROS: + return TimeUnit.MICROSECONDS; + case MILLIS: + return TimeUnit.MILLISECONDS; + case SECONDS: + return TimeUnit.SECONDS; + case MINUTES: + return TimeUnit.MINUTES; + case HOURS: + return TimeUnit.HOURS; + case DAYS: + return TimeUnit.DAYS; + default: + throw new UnsupportedOperationException("Unsupported ChronoUnit: " + chronoUnit); + } + }; } \ No newline at end of file diff --git a/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java b/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java index b13a11f73..b5112b76c 100644 --- a/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java +++ b/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; +import java.util.UUID; import java.util.concurrent.TimeUnit; @Repository @@ -13,18 +14,14 @@ public class RedisRepository { public RedisRepository(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } - public void save(String key, String value) { - ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(key, value); - } - public void save(String key, String value, Long expireTime, TimeUnit timeUnit) { + public void saveRefreshToken(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit) { ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(key, value, expireTime, timeUnit); + valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); } - public String get(String key) { + public String getRefreshToken(UUID memberId) { ValueOperations valueOperations = redisTemplate.opsForValue(); - return valueOperations.get(key); + return valueOperations.get(memberId.toString()); } -} +} \ No newline at end of file From af4e6a97c58b695f6ce9321a4a70885e5adbe37e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Oct 2024 20:58:26 +0900 Subject: [PATCH 080/478] =?UTF-8?q?feat:=20(#28)=20JwtService=EC=97=90?= =?UTF-8?q?=EC=84=9C=20refreshToken=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=A0?= =?UTF-8?q?=20=EB=95=8C=20Redis=EC=97=90=20=EC=83=9D=EC=84=B1=20=EB=90=9C?= =?UTF-8?q?=20refreshToken=EC=9D=84=20=EC=A0=80=EC=9E=A5=ED=95=9C=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/application/JwtService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index c4ff710e0..97f49c341 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -35,11 +35,13 @@ public enum Type { private final SecretKey SECRET_KEY; private final long ACCESS_EXPIRATION; private final long REFRESH_EXPIRATION; + private final RedisService redisService; - public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry) { + public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry, RedisService redisService) { this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.ACCESS_EXPIRATION = accessTokenExpiry; this.REFRESH_EXPIRATION = refreshTokenExpiry; + this.redisService = redisService; } public String provideAccessToken(Member member) { @@ -52,12 +54,16 @@ public String provideAccessToken(Member member) { } public String provideRefreshToken(Member member) { - return provideToken( + String refreshToken = provideToken( member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION ); + + redisService.saveRefreshToken(member.getId(), refreshToken, REFRESH_EXPIRATION, ChronoUnit.DAYS); + + return refreshToken; } public Claims getPayload(String token) { From 33d0214f0216099d412cd690e49e3f0fdda7e062 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 01:49:30 +0900 Subject: [PATCH 081/478] =?UTF-8?q?feat:=20(#28)=20RedisService=EC=97=90?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/RedisService.java | 26 ++++++++++++++----- .../core/exception/error/GlobalErrorCode.java | 4 ++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/core/application/RedisService.java b/src/main/java/spring/backend/core/application/RedisService.java index e023efff7..809a9549b 100644 --- a/src/main/java/spring/backend/core/application/RedisService.java +++ b/src/main/java/spring/backend/core/application/RedisService.java @@ -1,7 +1,9 @@ package spring.backend.core.application; +import io.lettuce.core.RedisConnectionException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.infrastructure.redis.repository.RedisRepository; import java.time.temporal.ChronoUnit; @@ -14,15 +16,27 @@ public class RedisService { private final RedisRepository redisRepository; public void saveRefreshToken(UUID memberId, String refreshToken, Long expireTime, ChronoUnit chronoUnit) { - TimeUnit timeUnit = convertChronoUnitToTimeUnit(chronoUnit); - redisRepository.saveRefreshToken(memberId, refreshToken, expireTime, timeUnit); + try { + TimeUnit timeUnit = convertChronoUnitToTimeUnit(chronoUnit); + redisRepository.saveRefreshToken(memberId, refreshToken, expireTime, timeUnit); + } catch (RedisConnectionException e) { + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } } public String getRefreshToken(UUID memberId) { - return redisRepository.getRefreshToken(memberId); + try { + return redisRepository.getRefreshToken(memberId); + } catch (RedisConnectionException e) { + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } } - private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { + public TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { switch (chronoUnit) { case NANOS: return TimeUnit.NANOSECONDS; @@ -39,7 +53,7 @@ private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { case DAYS: return TimeUnit.DAYS; default: - throw new UnsupportedOperationException("Unsupported ChronoUnit: " + chronoUnit); + throw GlobalErrorCode.UNSUPPORTED_TYPE.toException(); } - }; + } } \ No newline at end of file diff --git a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java index c19fceb91..8a828872c 100644 --- a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java +++ b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java @@ -9,7 +9,9 @@ public enum GlobalErrorCode implements BaseErrorCode { ALREADY_PROCESS_STARTED(HttpStatus.BAD_REQUEST, "이미 처리중인 요청입니다."), - INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 내부 오류입니다."); + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 내부 오류입니다."), + REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버와의 연결에 문제가 발생했습니다."), + UNSUPPORTED_TYPE(HttpStatus.BAD_REQUEST, "올바르지 않은 타입입니다."); private final HttpStatus httpStatus; From 6ecb0d20e8c709b563b62f535153bb786731c92b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 01:59:49 +0900 Subject: [PATCH 082/478] =?UTF-8?q?feat:=20(#28)=20RedisServiceTest?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/RedisServiceTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/test/java/spring/backend/core/application/RedisServiceTest.java diff --git a/src/test/java/spring/backend/core/application/RedisServiceTest.java b/src/test/java/spring/backend/core/application/RedisServiceTest.java new file mode 100644 index 000000000..b4aeff874 --- /dev/null +++ b/src/test/java/spring/backend/core/application/RedisServiceTest.java @@ -0,0 +1,44 @@ +package spring.backend.core.application; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class RedisServiceTest { + @Autowired + private RedisService redisService; + + @Autowired + private JwtService jwtService; + + private final UUID memberId = UUID.randomUUID(); + + @AfterAll + static void afterAll(@Qualifier("redisConnectionFactory") LettuceConnectionFactory connectionFactory) { + connectionFactory.getConnection().flushDb(); + } + + @DisplayName("RefreshToken이 발급될 때 ID와 RefreshToken를 Redis에 저장된다") + @Test + void saveRefreshTokenWhenTokenReleased() { + // given + Member member = Member.builder() + .id(memberId) + .email("test@test.com") + .build(); + // when + String refreshToken = jwtService.provideRefreshToken(member); + // then + assertThat(refreshToken).isEqualTo(redisService.getRefreshToken(member.getId())); + } +} \ No newline at end of file From 48c1281740e2530fadcb2c4cd4e2ff6a72d2e34c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 12:48:58 +0900 Subject: [PATCH 083/478] =?UTF-8?q?feat:=20(#28)=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=EA=B3=BC=20Redis=EB=A5=BC?= =?UTF-8?q?=20docker-compose.yml=EB=A1=9C=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=ED=99=94=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-local.yml | 29 +++++++++++++++++++ docker-compose.yml | 26 +++++++++++++---- .../redis/RedisConfiguration.java | 1 - 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 docker-compose-local.yml diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 000000000..c9a9e8bca --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,29 @@ +services: + cnergy-backend: + build: + context: . + dockerfile: Dockerfile + ports: + - '8080:8080' + environment: + SPRING_PROFILES_ACTIVE: local + depends_on: + - redis + networks: + - cnergy-backend-network + + redis: + image: redis:6.0.9 + ports: + - '6379:6379' + volumes: + - redis-data:/data + networks: + - cnergy-backend-network + +volumes: + redis-data: + +networks: + cnergy-backend-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 5b438c12a..6bc6c7698 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,25 @@ services: cnergy-backend: - build: - context: . - dockerfile: Dockerfile + image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" ports: - '8080:8080' - environment: - SPRING_PROFILES_ACTIVE: local + depends_on: + - redis + networks: + - cnergy-backend-network + + redis: + image: redis:6.0.9 + ports: + - '6379:6379' + volumes: + - redis-data:/data + networks: + - cnergy-backend-network + +volumes: + redis-data: + +networks: + cnergy-backend-network: + driver: bridge \ No newline at end of file diff --git a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java index 58a7fb009..f195716a4 100644 --- a/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/redis/RedisConfiguration.java @@ -20,7 +20,6 @@ public LettuceConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); } - @Bean public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); From ac6cc3c305834e906080a94a7383b254ff9830af Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 12:52:17 +0900 Subject: [PATCH 084/478] =?UTF-8?q?feat:=20(#28)=20docker-compose=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=B4=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 067b6ddfe..c64ce91a2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,23 +28,6 @@ jobs: java-version: '17' distribution: 'temurin' - - name: YML 파일 세팅 - env: - APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} - TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} - run: | - cd ./src - rm -rf main/resources/application.yml - mkdir -p test/resources - echo "$APPLICATION_PROPERTIES" > main/resources/application.yml - echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml - - - name: gradlew 권한 부여 - run: chmod +x gradlew - - - name: 테스트 수행 - run: ./gradlew test - - name: 스프링부트 빌드 run: ./gradlew build @@ -68,8 +51,10 @@ jobs: tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} platforms: linux/amd64,linux/arm64 - - name: NCP 접속 후 이미지 다운로드 및 배포 - if: success() + - name: Docker Compose 파일 NCP 서버로 전송 + run: scp -o StrictHostKeyChecking=no -P ${{ secrets.NCP_PORT }} docker-compose.yml ${{ secrets.NCP_USERNAME }}@${{ secrets.NCP_HOST }}:./ + + - name: NCP 접속 후 배포 uses: appleboy/ssh-action@master with: host: ${{ secrets.NCP_HOST }} @@ -77,8 +62,7 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | - docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} sudo chmod +x ./deploy.sh export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} export GITHUB_SHA=${{ github.sha }} - ./deploy.sh \ No newline at end of file + ./deploy.sh From 782516c701f76c673087ee5dbc535b90e00d4e25 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 13:15:56 +0900 Subject: [PATCH 085/478] =?UTF-8?q?feat:=20(#28)=20ci=20=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=20=EC=A4=91=20redis=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20ci=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c64ce91a2..644c5fd95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,6 +8,12 @@ jobs: ci: runs-on: ubuntu-latest + services: + redis: + image: redis + ports: + - 6379:6379 + steps: - name: Checkout source code uses: actions/checkout@v4 From b1298c07c988d915e61ce66a69b05ef0010fde55 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Oct 2024 20:17:26 +0900 Subject: [PATCH 086/478] =?UTF-8?q?feat:=20(#28)=20=EB=A1=9C=EC=BB=AC?= =?UTF-8?q?=EC=9A=A9=20Dockerfile-local,=20docker-compose-local.yml=20?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile-local | 17 +++++++++++++++++ docker-compose-local.yml | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Dockerfile-local diff --git a/Dockerfile-local b/Dockerfile-local new file mode 100644 index 000000000..d5afb068d --- /dev/null +++ b/Dockerfile-local @@ -0,0 +1,17 @@ +FROM gradle:8.10.1-jdk17 AS build + +WORKDIR /app + +COPY . /app + +RUN gradle clean build -x test --no-daemon + +FROM openjdk:17.0.1-jdk-slim + +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar /app/backend.jar + +EXPOSE 8080 +ENTRYPOINT ["java"] +CMD ["-jar", "backend.jar"] \ No newline at end of file diff --git a/docker-compose-local.yml b/docker-compose-local.yml index c9a9e8bca..15b79237d 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -2,13 +2,14 @@ services: cnergy-backend: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile-local ports: - '8080:8080' environment: SPRING_PROFILES_ACTIVE: local depends_on: - - redis + - redis + - mysql networks: - cnergy-backend-network @@ -21,8 +22,21 @@ services: networks: - cnergy-backend-network + mysql: + image: mysql + ports: + - '3307:3306' + volumes: + - mysql-data:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: cnergy + networks: + - cnergy-backend-network + volumes: redis-data: + mysql-data: networks: cnergy-backend-network: From 630d89e514fc4ab55941e13240e6f099c20189f7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 10:43:09 +0900 Subject: [PATCH 087/478] =?UTF-8?q?fix=20:=20(#28)=20docker-compose-local.?= =?UTF-8?q?yml=EC=97=90=20mysql=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-local.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 15b79237d..7870552c3 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -9,7 +9,6 @@ services: SPRING_PROFILES_ACTIVE: local depends_on: - redis - - mysql networks: - cnergy-backend-network @@ -22,21 +21,8 @@ services: networks: - cnergy-backend-network - mysql: - image: mysql - ports: - - '3307:3306' - volumes: - - mysql-data:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: cnergy - networks: - - cnergy-backend-network - volumes: redis-data: - mysql-data: networks: cnergy-backend-network: From 156d99a888f40959d08d61a7776854ba0e3d2d17 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:34:48 +0900 Subject: [PATCH 088/478] =?UTF-8?q?fix=20:=20(#28)=20Redis=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20GlobalErrorCode=EC=97=90=EC=84=9C=20Authen?= =?UTF-8?q?ticationErrorCode=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/exception/AuthenticationErrorCode.java | 4 +++- .../spring/backend/core/exception/error/GlobalErrorCode.java | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index add6cb6bc..944bf2e02 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -21,7 +21,9 @@ public enum AuthenticationErrorCode implements BaseErrorCode { NOT_EXIST_AUTH_CODE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 제공받은 인증 코드가 존재하지 않습니다."), ACCESS_TOKEN_NOT_ISSUED(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 액세스 토큰이 발급되지 않았습니다."), NOT_EXIST_RESOURCE_RESPONSE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 리소스 서버에서 자원이 존재하지 않습니다."), - RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."); + RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."), + UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."); + private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java index 8a828872c..92a37cab7 100644 --- a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java +++ b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java @@ -10,9 +10,7 @@ public enum GlobalErrorCode implements BaseErrorCode { ALREADY_PROCESS_STARTED(HttpStatus.BAD_REQUEST, "이미 처리중인 요청입니다."), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 내부 오류입니다."), - REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버와의 연결에 문제가 발생했습니다."), - UNSUPPORTED_TYPE(HttpStatus.BAD_REQUEST, "올바르지 않은 타입입니다."); - + REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버와의 연결에 문제가 발생했습니다."); private final HttpStatus httpStatus; private final String message; From 261677f7a6027d9b3c20c549fddbd03d2e98e888 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:35:59 +0900 Subject: [PATCH 089/478] =?UTF-8?q?fix=20:=20(#28)=20RefreshTokenRepositor?= =?UTF-8?q?y=20=EA=B4=80=EB=A0=A8=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EC=99=80=20=EA=B5=AC=ED=98=84=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=A7=8C=EB=93=AD=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RefreshTokenRepository.java | 9 ++++++ .../RefreshTokenRedisRepository.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java create mode 100644 src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java new file mode 100644 index 000000000..bc1d5dcbc --- /dev/null +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package spring.backend.auth.domain.repository; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public interface RefreshTokenRepository { + void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit); + String findById(UUID memberId); +} diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java new file mode 100644 index 000000000..10d039c3f --- /dev/null +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -0,0 +1,28 @@ +package spring.backend.auth.infrastructure.redis.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Repository; +import spring.backend.auth.domain.repository.RefreshTokenRepository; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Repository +@RequiredArgsConstructor +public class RefreshTokenRedisRepository implements RefreshTokenRepository { + private final RedisTemplate redisTemplate; + + @Override + public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); + } + + @Override + public String findById(UUID memberId) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(memberId.toString()); + } +} From 764e9bf7c22df8d6efbb890143466a482512f483 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:36:27 +0900 Subject: [PATCH 090/478] =?UTF-8?q?fix=20:=20(#28)=20RedisRepository?= =?UTF-8?q?=EC=99=80=20RedisService=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/RedisService.java | 59 ------------------- .../redis/repository/RedisRepository.java | 27 --------- 2 files changed, 86 deletions(-) delete mode 100644 src/main/java/spring/backend/core/application/RedisService.java delete mode 100644 src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java diff --git a/src/main/java/spring/backend/core/application/RedisService.java b/src/main/java/spring/backend/core/application/RedisService.java deleted file mode 100644 index 809a9549b..000000000 --- a/src/main/java/spring/backend/core/application/RedisService.java +++ /dev/null @@ -1,59 +0,0 @@ -package spring.backend.core.application; - -import io.lettuce.core.RedisConnectionException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.core.infrastructure.redis.repository.RedisRepository; - -import java.time.temporal.ChronoUnit; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -@Service -@RequiredArgsConstructor -public class RedisService { - private final RedisRepository redisRepository; - - public void saveRefreshToken(UUID memberId, String refreshToken, Long expireTime, ChronoUnit chronoUnit) { - try { - TimeUnit timeUnit = convertChronoUnitToTimeUnit(chronoUnit); - redisRepository.saveRefreshToken(memberId, refreshToken, expireTime, timeUnit); - } catch (RedisConnectionException e) { - throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); - } catch (Exception e) { - throw GlobalErrorCode.INTERNAL_ERROR.toException(); - } - } - - public String getRefreshToken(UUID memberId) { - try { - return redisRepository.getRefreshToken(memberId); - } catch (RedisConnectionException e) { - throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); - } catch (Exception e) { - throw GlobalErrorCode.INTERNAL_ERROR.toException(); - } - } - - public TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { - switch (chronoUnit) { - case NANOS: - return TimeUnit.NANOSECONDS; - case MICROS: - return TimeUnit.MICROSECONDS; - case MILLIS: - return TimeUnit.MILLISECONDS; - case SECONDS: - return TimeUnit.SECONDS; - case MINUTES: - return TimeUnit.MINUTES; - case HOURS: - return TimeUnit.HOURS; - case DAYS: - return TimeUnit.DAYS; - default: - throw GlobalErrorCode.UNSUPPORTED_TYPE.toException(); - } - } -} \ No newline at end of file diff --git a/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java b/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java deleted file mode 100644 index b5112b76c..000000000 --- a/src/main/java/spring/backend/core/infrastructure/redis/repository/RedisRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -package spring.backend.core.infrastructure.redis.repository; - -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Repository; - -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -@Repository -public class RedisRepository { - private final RedisTemplate redisTemplate; - - public RedisRepository(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - public void saveRefreshToken(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit) { - ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); - } - - public String getRefreshToken(UUID memberId) { - ValueOperations valueOperations = redisTemplate.opsForValue(); - return valueOperations.get(memberId.toString()); - } -} \ No newline at end of file From dfdde008aa5db37b9fdfacc3839994e99eb62591 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:36:59 +0900 Subject: [PATCH 091/478] =?UTF-8?q?fix=20:=20(#28)=20RefreshTokenService?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/main/java/spring/backend/auth/application/RefreshTokenService.java diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java new file mode 100644 index 000000000..ac9111e28 --- /dev/null +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -0,0 +1,69 @@ +package spring.backend.auth.application; + +import io.lettuce.core.RedisConnectionException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import spring.backend.auth.domain.repository.RefreshTokenRepository; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.application.JwtService; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.member.domain.entity.Member; + +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Service +public class RefreshTokenService { + private final JwtService jwtService; + private final long REFRESH_TOKEN_EXPIRATION; + private final RefreshTokenRepository refreshTokenRepository; + + public RefreshTokenService(JwtService jwtService, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry, RefreshTokenRepository refreshTokenRepository) { + this.jwtService = jwtService; + this.REFRESH_TOKEN_EXPIRATION = refreshTokenExpiry; + this.refreshTokenRepository = refreshTokenRepository; + } + + public String saveRefreshToken(Member member) { + try { + refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member),REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); + return getRefreshToken(member.getId()); + } catch (RedisConnectionException e) { + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } + + public String getRefreshToken(UUID memberId) { + try { + return refreshTokenRepository.findById(memberId); + } catch (RedisConnectionException e) { + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } + + private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { + switch (chronoUnit) { + case NANOS: + return TimeUnit.NANOSECONDS; + case MICROS: + return TimeUnit.MICROSECONDS; + case MILLIS: + return TimeUnit.MILLISECONDS; + case SECONDS: + return TimeUnit.SECONDS; + case MINUTES: + return TimeUnit.MINUTES; + case HOURS: + return TimeUnit.HOURS; + case DAYS: + return TimeUnit.DAYS; + default: + throw AuthenticationErrorCode.UNSUPPORTED_REDIS_TIME_TYPE.toException(); + } + } +} From d159d3b1b9ff4adadf80fb7040adb94860f7afbe Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:37:20 +0900 Subject: [PATCH 092/478] =?UTF-8?q?fix=20:=20(#28)=20JwtService=EC=97=90?= =?UTF-8?q?=EC=84=9C=20RedisService=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/application/JwtService.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index 97f49c341..c4ff710e0 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -35,13 +35,11 @@ public enum Type { private final SecretKey SECRET_KEY; private final long ACCESS_EXPIRATION; private final long REFRESH_EXPIRATION; - private final RedisService redisService; - public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry, RedisService redisService) { + public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.access-token-expiry}") long accessTokenExpiry, @Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry) { this.SECRET_KEY = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); this.ACCESS_EXPIRATION = accessTokenExpiry; this.REFRESH_EXPIRATION = refreshTokenExpiry; - this.redisService = redisService; } public String provideAccessToken(Member member) { @@ -54,16 +52,12 @@ public String provideAccessToken(Member member) { } public String provideRefreshToken(Member member) { - String refreshToken = provideToken( + return provideToken( member.getEmail(), member.getId(), Type.REFRESH, REFRESH_EXPIRATION ); - - redisService.saveRefreshToken(member.getId(), refreshToken, REFRESH_EXPIRATION, ChronoUnit.DAYS); - - return refreshToken; } public Claims getPayload(String token) { From e1e4db5cfb87e8a8ebe046b4dbe69a635811c6ee Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:37:52 +0900 Subject: [PATCH 093/478] =?UTF-8?q?fix=20:=20(#28)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20RefreshTokenService=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=B4=20=ED=86=A0=ED=81=B0=EC=9D=84=20Red?= =?UTF-8?q?is=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 41cede529..75d7cac4a 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -26,6 +26,8 @@ public class HandleOAuthLoginService { private final JwtService jwtService; + private final RefreshTokenService refreshTokenService; + public LoginResponse handleOAuthLogin(String providerName, String code, String state) { if (providerName == null || providerName.isEmpty()) { throw AuthenticationErrorCode.NOT_EXIST_PROVIDER.toException(); @@ -52,7 +54,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - return new LoginResponse(jwtService.provideAccessToken(member), jwtService.provideRefreshToken(member), member.getRole()); - + return new LoginResponse(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member.getRole()); } } From b7c41a1504abca7bc684649882f87727e93fe153 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 21:38:11 +0900 Subject: [PATCH 094/478] =?UTF-8?q?fix=20:=20(#28)=20RefreshTokenServiceTe?= =?UTF-8?q?st=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Test.java => RefreshTokenServiceTest.java} | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) rename src/test/java/spring/backend/core/application/{RedisServiceTest.java => RefreshTokenServiceTest.java} (54%) diff --git a/src/test/java/spring/backend/core/application/RedisServiceTest.java b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java similarity index 54% rename from src/test/java/spring/backend/core/application/RedisServiceTest.java rename to src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java index b4aeff874..232ddc4c7 100644 --- a/src/test/java/spring/backend/core/application/RedisServiceTest.java +++ b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java @@ -1,12 +1,13 @@ package spring.backend.core.application; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import spring.backend.auth.application.RefreshTokenService; import spring.backend.member.domain.entity.Member; import java.util.UUID; @@ -14,14 +15,25 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest -public class RedisServiceTest { +public class RefreshTokenServiceTest { @Autowired - private RedisService redisService; + private RefreshTokenService refreshTokenService; + + private final UUID memberId = UUID.randomUUID(); @Autowired - private JwtService jwtService; + private RedisTemplate redisTemplate; + + @BeforeEach + public void checkRedisConnection() { + try { + redisTemplate.getConnectionFactory().getConnection(); + System.out.println("Redis 연결 성공"); + } catch (RedisConnectionFailureException e) { + System.err.println("Redis 연결 실패: " + e.getMessage()); + } + } - private final UUID memberId = UUID.randomUUID(); @AfterAll static void afterAll(@Qualifier("redisConnectionFactory") LettuceConnectionFactory connectionFactory) { @@ -36,9 +48,7 @@ void saveRefreshTokenWhenTokenReleased() { .id(memberId) .email("test@test.com") .build(); - // when - String refreshToken = jwtService.provideRefreshToken(member); - // then - assertThat(refreshToken).isEqualTo(redisService.getRefreshToken(member.getId())); + // when & then + assertThat(refreshTokenService.saveRefreshToken(member)).isEqualTo(refreshTokenService.getRefreshToken(memberId)); } } \ No newline at end of file From 4f4e42f0cbb258e72881cd88332b308d92da152e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Oct 2024 23:34:42 +0900 Subject: [PATCH 095/478] =?UTF-8?q?fix=20:=20(#28)=20RefreshTokenRedisRepo?= =?UTF-8?q?sitory=EC=9D=98=20findById=EB=A5=BC=20findByMemberId=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/application/RefreshTokenService.java | 2 +- .../backend/auth/domain/repository/RefreshTokenRepository.java | 2 +- .../redis/repository/RefreshTokenRedisRepository.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index ac9111e28..381da4bd2 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -38,7 +38,7 @@ public String saveRefreshToken(Member member) { public String getRefreshToken(UUID memberId) { try { - return refreshTokenRepository.findById(memberId); + return refreshTokenRepository.findByMemberId(memberId); } catch (RedisConnectionException e) { throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); } catch (Exception e) { diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index bc1d5dcbc..9e04e6738 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -5,5 +5,5 @@ public interface RefreshTokenRepository { void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit); - String findById(UUID memberId); + String findByMemberId(UUID memberId); } diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index 10d039c3f..92f06207f 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -21,7 +21,7 @@ public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit t } @Override - public String findById(UUID memberId) { + public String findByMemberId(UUID memberId) { ValueOperations valueOperations = redisTemplate.opsForValue(); return valueOperations.get(memberId.toString()); } From 68b3a410d9917d2dab9e9e216b9eec6bc5e3aa11 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 02:25:41 +0900 Subject: [PATCH 096/478] =?UTF-8?q?fix:=20(#37)=20docker-compose=EB=A5=BC?= =?UTF-8?q?=20=ED=86=B5=ED=95=B4=20=EB=B0=B0=ED=8F=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 644c5fd95..4a7280745 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,6 +11,11 @@ jobs: services: redis: image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 6379:6379 @@ -18,6 +23,11 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Set up SSH key + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.NCP_SSH_PRIVATE_KEY }} + - name: Gradle 캐시 적용 uses: actions/cache@v3 with: @@ -34,6 +44,23 @@ jobs: java-version: '17' distribution: 'temurin' + - name: YML 파일 세팅 + env: + APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} + TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} + run: | + cd ./src + rm -rf main/resources/application.yml + mkdir -p test/resources + echo "$APPLICATION_PROPERTIES" > main/resources/application.yml + echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml + + - name: gradlew 권한 부여 + run: chmod +x gradlew + + - name: 테스트 수행 + run: ./gradlew test + - name: 스프링부트 빌드 run: ./gradlew build @@ -68,7 +95,7 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | + echo "NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }}" >> .env + echo "GITHUB_SHA=${{ github.sha }}" >> .env sudo chmod +x ./deploy.sh - export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} - export GITHUB_SHA=${{ github.sha }} ./deploy.sh From 802123bb353113a75b49bd8567865f18880c6289 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 02:50:07 +0900 Subject: [PATCH 097/478] =?UTF-8?q?fix:=20(#39)=20docker-compose=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EA=B3=A0=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 6bc6c7698..0f4dfcf15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: cnergy-backend: image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + container_name: cnergy-backend ports: - '8080:8080' depends_on: @@ -10,6 +11,7 @@ services: redis: image: redis:6.0.9 + container_name: redis ports: - '6379:6379' volumes: From 76670749e424a464e69d958ce5adc730544f5195 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 03:02:36 +0900 Subject: [PATCH 098/478] =?UTF-8?q?fix:=20(#39)=20CI=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4a7280745..b368e9273 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -95,7 +95,7 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | - echo "NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }}" >> .env - echo "GITHUB_SHA=${{ github.sha }}" >> .env + export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} + export GITHUB_SHA=${{ github.sha }} sudo chmod +x ./deploy.sh ./deploy.sh From ebd45920d70a92bee9868555068e4308c505817f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 01:01:50 +0900 Subject: [PATCH 099/478] =?UTF-8?q?feat:=20(#42)=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20CORS?= =?UTF-8?q?=EB=A5=BC=20=ED=97=88=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/WebMvcConfiguration.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index 2c2d990c1..b0297114e 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import spring.backend.core.configuration.argumentresolver.LoginMemberArgumentResolver; @@ -18,6 +19,15 @@ public class WebMvcConfiguration implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000", "https://cnergy.kro.kr") + .allowedMethods("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowCredentials(true) + .maxAge(3000); + } + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authorizationInterceptor); From c1fa0c383b915d854baa485cffcd32462f09276e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 07:43:10 +0900 Subject: [PATCH 100/478] =?UTF-8?q?feat:=20(#44)=20MemberRepository?= =?UTF-8?q?=EC=97=90=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/repository/MemberRepository.java | 2 ++ .../persistence/jpa/adapter/MemberRepositoryImpl.java | 6 ++++++ .../persistence/jpa/repository/MemberJpaRepository.java | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java index 14ffec7ef..46dc09749 100644 --- a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java +++ b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java @@ -1,6 +1,7 @@ package spring.backend.member.domain.repository; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Role; import java.util.List; import java.util.UUID; @@ -11,4 +12,5 @@ public interface MemberRepository { Member save(Member member); Member findByEmail(String email); List findAllByEmail(String email); + boolean existsByNicknameAndRole(String nickname, Role role); } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java index 8d03339fd..729bc7054 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Role; import spring.backend.member.exception.MemberErrorCode; import spring.backend.member.infrastructure.mapper.MemberMapper; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; @@ -58,4 +59,9 @@ public List findAllByEmail(String email) { } return memberJpaEntities.stream().map(memberMapper::toDomainEntity).collect(Collectors.toList()); } + + @Override + public boolean existsByNicknameAndRole(String nickname, Role role) { + return memberJpaRepository.existsByNicknameAndRole(nickname, role); + } } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java index 34d73ea8b..20f02a2ba 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java @@ -1,6 +1,7 @@ package spring.backend.member.infrastructure.persistence.jpa.repository; import org.springframework.data.jpa.repository.JpaRepository; +import spring.backend.member.domain.value.Role; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; import java.util.List; @@ -11,4 +12,6 @@ public interface MemberJpaRepository extends JpaRepository findAllByEmail(String email); + + boolean existsByNicknameAndRole(String nickname, Role role); } From 92866f380a03c2cab8bb4a81cde4fc1155286dd0 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 07:43:54 +0900 Subject: [PATCH 101/478] =?UTF-8?q?feat:=20(#44)=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EA=B2=80=EC=A6=9D=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20API=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ValidateNicknameService.java | 35 +++++++++++++++++++ .../member/exception/MemberErrorCode.java | 6 +++- .../ValidateNicknameController.java | 22 ++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/member/application/ValidateNicknameService.java create mode 100644 src/main/java/spring/backend/member/presentation/ValidateNicknameController.java diff --git a/src/main/java/spring/backend/member/application/ValidateNicknameService.java b/src/main/java/spring/backend/member/application/ValidateNicknameService.java new file mode 100644 index 000000000..d7e0fad32 --- /dev/null +++ b/src/main/java/spring/backend/member/application/ValidateNicknameService.java @@ -0,0 +1,35 @@ +package spring.backend.member.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Role; +import spring.backend.member.exception.MemberErrorCode; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class ValidateNicknameService { + + private final MemberRepository memberRepository; + + public boolean validateNickname(String nickname) { + if (nickname == null || nickname.isBlank()) { + log.error("[ValidateNicknameService] Nickname is empty"); + throw MemberErrorCode.NOT_EXIST_NICKNAME.toException(); + } + if (nickname.length() > 6) { + log.error("[ValidateNicknameService] Nickname is smaller than 6 characters"); + throw MemberErrorCode.INVALID_NICKNAME_LENGTH.toException(); + } + if (!nickname.matches("^[a-zA-Z0-9가-힣]+$")) { + log.error("[ValidateNicknameService] Nickname is invalid"); + throw MemberErrorCode.INVALID_NICKNAME_FORMAT.toException(); + } + if (memberRepository.existsByNicknameAndRole(nickname, Role.MEMBER)) { + throw MemberErrorCode.ALREADY_REGISTERED_NICKNAME.toException(); + } + return true; + } +} diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index 6348f47a6..ff8ca0bea 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -13,7 +13,11 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_EXIST_CONDITION(HttpStatus.BAD_REQUEST, "요청 조건이 존재하지 않습니다."), ALREADY_REGISTERED_WITH_DIFFERENT_OAUTH2(HttpStatus.BAD_REQUEST, "이미 다른 소셜 로그인으로 가입된 계정입니다."), MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."), - NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."); + NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."), + NOT_EXIST_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임은 필수 입력값입니다."), + INVALID_NICKNAME_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 1자에서 6자 사이여야 합니다."), + INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영문, 숫자 조합이어야 합니다."), + ALREADY_REGISTERED_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임이 이미 사용 중입니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java new file mode 100644 index 000000000..4576db419 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java @@ -0,0 +1,22 @@ +package spring.backend.member.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.application.ValidateNicknameService; + +@RestController +@RequiredArgsConstructor +public class ValidateNicknameController { + + private final ValidateNicknameService validateNicknameService; + + @GetMapping("/v1/members/check-nickname") + public ResponseEntity> validateNickname(@RequestParam String nickname) { + boolean isValidNickname = validateNicknameService.validateNickname(nickname); + return ResponseEntity.ok(new RestResponse<>(isValidNickname)); + } +} From 2f33df8aa78d2e1964fb43be0dcd28431946fbe6 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 17:28:50 +0900 Subject: [PATCH 102/478] =?UTF-8?q?feat:=20(#44)=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EA=B2=80=EC=A6=9D=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ValidateNicknameController.java | 6 ++--- .../swagger/ValidateNicknameSwagger.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java diff --git a/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java index 4576db419..d4ad8e94f 100644 --- a/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java +++ b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java @@ -3,19 +3,19 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import spring.backend.core.presentation.RestResponse; import spring.backend.member.application.ValidateNicknameService; +import spring.backend.member.presentation.swagger.ValidateNicknameSwagger; @RestController @RequiredArgsConstructor -public class ValidateNicknameController { +public class ValidateNicknameController implements ValidateNicknameSwagger { private final ValidateNicknameService validateNicknameService; @GetMapping("/v1/members/check-nickname") - public ResponseEntity> validateNickname(@RequestParam String nickname) { + public ResponseEntity> validateNickname(String nickname) { boolean isValidNickname = validateNicknameService.validateNickname(nickname); return ResponseEntity.ok(new RestResponse<>(isValidNickname)); } diff --git a/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java new file mode 100644 index 000000000..9ce73b0c4 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java @@ -0,0 +1,22 @@ +package spring.backend.member.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.exception.MemberErrorCode; + +@Tag(name = "Member", description = "멤버") +public interface ValidateNicknameSwagger { + + @Operation( + summary = "닉네임 중복 검증 API", + description = "닉네임이 조건에 충족하지 않거나 중복일 경우, 에러를 발생합니다.", + operationId = "/v1/members/check-nickname" + ) + @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) + ResponseEntity> validateNickname(@Schema(description = "요청 닉네임", example = "조각조각") String nickname); +} From fc0a0e07ad1aafe0e99db5ac635e7c5f00339b0d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 18:17:13 +0900 Subject: [PATCH 103/478] =?UTF-8?q?feat:=20(#44)=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=A6=9D=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ValidateNicknameServiceTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java diff --git a/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java b/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java new file mode 100644 index 000000000..74887e6ce --- /dev/null +++ b/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java @@ -0,0 +1,87 @@ +package spring.backend.member.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Role; +import spring.backend.member.exception.MemberErrorCode; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ValidateNicknameServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private ValidateNicknameService validateNicknameService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("닉네임이 공백일 때 예외가 발생한다.") + void throwExceptionWhenNicknameIsBlank() { + // Given + String nickname = " "; + + // When + DomainException blankException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); + + // Then + assertEquals(MemberErrorCode.NOT_EXIST_NICKNAME.name(), blankException.getCode()); + } + + @Test + @DisplayName("닉네임 길이가 6자를 초과할 때 예외가 발생한다.") + void throwExceptionWhenNicknameLengthIsInvalid() { + // Given + String nickname = "1234567"; + + // When + DomainException longLengthException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); + + // Then + assertEquals(MemberErrorCode.INVALID_NICKNAME_LENGTH.name(), longLengthException.getCode()); + } + + @Test + @DisplayName("닉네임 형식이 유효하지 않을 때 예외가 발생한다.") + void throwExceptionWhenNicknameFormatIsInvalid() { + // Given + String nickname = "조각ㅈㄱ"; + + // When + DomainException formatException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); + + // Then + assertEquals(MemberErrorCode.INVALID_NICKNAME_FORMAT.name(), formatException.getCode()); + } + + @Test + @DisplayName("이미 등록된 닉네임일 경우 예외가 발생한다.") + void throwExceptionWhenNicknameIsAlreadyRegistered() { + // Given + String nickname = "등록된이름"; + when(memberRepository.existsByNicknameAndRole(nickname, Role.MEMBER)).thenReturn(true); + + // When + DomainException alreadyRegisteredException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); + + // Then + assertEquals(MemberErrorCode.ALREADY_REGISTERED_NICKNAME.name(), alreadyRegisteredException.getCode()); + + // Mock 객체 정상 동작 확인 + verify(memberRepository).existsByNicknameAndRole(nickname, Role.MEMBER); + } +} \ No newline at end of file From d13b445c3808be7f220f1bfea4dab8c3680397fa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 21 Oct 2024 20:34:58 +0900 Subject: [PATCH 104/478] =?UTF-8?q?fix:=20(#46)=20docker-compose=EA=B0=80?= =?UTF-8?q?=20.env=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0f4dfcf15..8e1e3fc58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ services: cnergy-backend: image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" container_name: cnergy-backend + env_file: + - .env ports: - '8080:8080' depends_on: From aa53f0ca27ac01d40e8b658dc1c2d10fde3cb5c8 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 22 Oct 2024 18:07:53 +0900 Subject: [PATCH 105/478] =?UTF-8?q?feat:=20(#48)=20Swagger=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=97=90=20Https=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/swagger/SwaggerConfiguration.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java index b093bfcab..176f2576b 100644 --- a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; @@ -14,12 +15,6 @@ import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme.In; import io.swagger.v3.oas.models.security.SecurityScheme.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,8 +22,12 @@ import spring.backend.core.exception.error.BaseErrorCode; import spring.backend.core.presentation.ErrorResponse; +import java.util.*; +import java.util.stream.Collectors; + @Configuration -@OpenAPIDefinition(info = @Info(title = "C-nergy API", description = "C-nergy : API 명세서", version = "v1.0.0")) +@OpenAPIDefinition(info = @Info(title = "조각조각 API", description = "조각조각 : API 명세서", version = "v1.0.0"), + servers = {@Server(url = "${springdoc.server-url}", description = "Https Server URL")}) public class SwaggerConfiguration { @Bean From f468fb6abf54acbf3e00864f015af6813b253c0d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 15:42:45 +0900 Subject: [PATCH 106/478] =?UTF-8?q?feat:=20(#35)=20Redis=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=82=AD=EC=A0=9C=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/repository/RefreshTokenRepository.java | 1 + .../redis/repository/RefreshTokenRedisRepository.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index 9e04e6738..5280d924d 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -6,4 +6,5 @@ public interface RefreshTokenRepository { void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit); String findByMemberId(UUID memberId); + void delete(UUID memberId); } diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index 92f06207f..b37943f8a 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -25,4 +25,9 @@ public String findByMemberId(UUID memberId) { ValueOperations valueOperations = redisTemplate.opsForValue(); return valueOperations.get(memberId.toString()); } + + @Override + public void delete(UUID memberId) { + redisTemplate.delete(memberId.toString()); + } } From 70a94677a1d0cb55982cffca36eeb6ca47aa5d7a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 15:43:49 +0900 Subject: [PATCH 107/478] =?UTF-8?q?feat:=20(#35)=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/exception/AuthenticationErrorCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 944bf2e02..c6d6b16a8 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -22,7 +22,9 @@ public enum AuthenticationErrorCode implements BaseErrorCode { ACCESS_TOKEN_NOT_ISSUED(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 제공자에서 액세스 토큰이 발급되지 않았습니다."), NOT_EXIST_RESOURCE_RESPONSE(HttpStatus.BAD_GATEWAY, "OAuth 써드파티 리소스 서버에서 자원이 존재하지 않습니다."), RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."), - UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."); + UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."), + MISMATCH_TOKEN_MEMBER(HttpStatus.UNAUTHORIZED, "토큰의 회원 ID와 요청한 회원 ID가 일치하지 않습니다."), + NOT_EXIST_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "리프레시 토큰이 저장소에 존재하지 않습니다."); private final HttpStatus httpStatus; From dcc25f7a7165f2170d8902035e68e4817b1dd65a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 15:44:55 +0900 Subject: [PATCH 108/478] =?UTF-8?q?feat:=20(#35)=20RefreshTokenService?= =?UTF-8?q?=EC=9D=98=20Test=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 22 ++++++++++++++++++- .../application/RefreshTokenServiceTest.java | 21 +++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 381da4bd2..b69ce6127 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -1,11 +1,13 @@ package spring.backend.auth.application; import io.lettuce.core.RedisConnectionException; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import spring.backend.auth.domain.repository.RefreshTokenRepository; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; +import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.member.domain.entity.Member; @@ -13,6 +15,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; +@Slf4j @Service public class RefreshTokenService { private final JwtService jwtService; @@ -38,7 +41,24 @@ public String saveRefreshToken(Member member) { public String getRefreshToken(UUID memberId) { try { - return refreshTokenRepository.findByMemberId(memberId); + String refreshToken = refreshTokenRepository.findByMemberId(memberId); + if (refreshToken == null || refreshToken.isEmpty()) { + log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); + } + return refreshToken; + } catch (RedisConnectionException e) { + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (DomainException e) { + throw e; + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } + + public void deleteRefreshToken(UUID memberId) { + try { + refreshTokenRepository.delete(memberId); } catch (RedisConnectionException e) { throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); } catch (Exception e) { diff --git a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java index 232ddc4c7..bf5c38901 100644 --- a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java @@ -8,11 +8,13 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import spring.backend.auth.application.RefreshTokenService; +import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @SpringBootTest public class RefreshTokenServiceTest { @@ -21,6 +23,11 @@ public class RefreshTokenServiceTest { private final UUID memberId = UUID.randomUUID(); + private final Member member = Member.builder() + .id(memberId) + .email("test@test.com") + .build(); + @Autowired private RedisTemplate redisTemplate; @@ -43,12 +50,16 @@ static void afterAll(@Qualifier("redisConnectionFactory") LettuceConnectionFacto @DisplayName("RefreshToken이 발급될 때 ID와 RefreshToken를 Redis에 저장된다") @Test void saveRefreshTokenWhenTokenReleased() { - // given - Member member = Member.builder() - .id(memberId) - .email("test@test.com") - .build(); // when & then assertThat(refreshTokenService.saveRefreshToken(member)).isEqualTo(refreshTokenService.getRefreshToken(memberId)); } + + @DisplayName("memberId에 해당하는 RefreshToken이 Redis에 저장되어 있지 않은 경우 에러를 반환한다.") + @Test + void throwExceptionWhenRefreshTokenIsNotInRedis() { + // when & then + assertThatThrownBy(() -> refreshTokenService.getRefreshToken(UUID.randomUUID())) + .isInstanceOf(DomainException.class) + .hasMessage("해당 리프레시 토큰이 저장소에 존재하지 않습니다."); + } } \ No newline at end of file From 349fff1ef770667b1abc32d0be886f498f54e5c7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 15:45:11 +0900 Subject: [PATCH 109/478] =?UTF-8?q?feat:=20(#35)=20RotateToken=EC=9D=98=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B3=A0,=20RotateTokenService=EB=A5=BC=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RotateTokenService.java | 24 +++++++++++++++++++ .../dto/response/RotateTokenResponse.java | 4 ++++ 2 files changed, 28 insertions(+) create mode 100644 src/main/java/spring/backend/auth/application/RotateTokenService.java create mode 100644 src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java diff --git a/src/main/java/spring/backend/auth/application/RotateTokenService.java b/src/main/java/spring/backend/auth/application/RotateTokenService.java new file mode 100644 index 000000000..f97f33ddd --- /dev/null +++ b/src/main/java/spring/backend/auth/application/RotateTokenService.java @@ -0,0 +1,24 @@ +package spring.backend.auth.application; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import spring.backend.auth.dto.response.RotateTokenResponse; +import spring.backend.core.application.JwtService; +import spring.backend.member.application.MemberService; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RotateTokenService { + private final MemberService memberService; + private final JwtService jwtService; + + public RotateTokenResponse rotateAccessToken(String refreshToken) { + UUID memberId = UUID.fromString(jwtService.getPayload(refreshToken).get("memberId", String.class)); + return new RotateTokenResponse(jwtService.provideAccessToken(memberService.findByMemberId(memberId))); + } +} diff --git a/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java b/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java new file mode 100644 index 000000000..fc76d7ea7 --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java @@ -0,0 +1,4 @@ +package spring.backend.auth.dto.response; + +public record RotateTokenResponse(String accessToken) { +} From 7c45d073da2ef7c4da9e0d416322c7e5cc57162e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 21:55:39 +0900 Subject: [PATCH 110/478] =?UTF-8?q?feat:=20(#35)=20RotateTokenController?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RotateTokenController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/spring/backend/auth/presentation/RotateTokenController.java diff --git a/src/main/java/spring/backend/auth/presentation/RotateTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateTokenController.java new file mode 100644 index 000000000..6ec4b2998 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/RotateTokenController.java @@ -0,0 +1,25 @@ +package spring.backend.auth.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.auth.application.RotateTokenService; +import spring.backend.auth.dto.response.RotateTokenResponse; +import spring.backend.core.presentation.RestResponse; + +@RestController +@RequestMapping("/v1/token/rotate") +@RequiredArgsConstructor +public class RotateTokenController { + private final RotateTokenService rotateTokenService; + + @GetMapping + public ResponseEntity> rotateAccessToken( + @CookieValue(name = "refreshToken", required = false) String refreshToken + ) { + return ResponseEntity.ok(new RestResponse<>(rotateTokenService.rotateAccessToken(refreshToken))); + } +} From eda8b3eba9740a652ae8891296ef30aa8f803cca Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 20 Oct 2024 21:56:21 +0900 Subject: [PATCH 111/478] =?UTF-8?q?feat:=20(#35)=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20memberId=EC=97=90=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20refreshToken=EC=9D=B4=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?exception=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/application/RefreshTokenServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java index bf5c38901..8f37d21fe 100644 --- a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java @@ -59,7 +59,6 @@ void saveRefreshTokenWhenTokenReleased() { void throwExceptionWhenRefreshTokenIsNotInRedis() { // when & then assertThatThrownBy(() -> refreshTokenService.getRefreshToken(UUID.randomUUID())) - .isInstanceOf(DomainException.class) - .hasMessage("해당 리프레시 토큰이 저장소에 존재하지 않습니다."); + .isInstanceOf(DomainException.class).hasMessage("리프레시 토큰이 저장소에 존재하지 않습니다."); } } \ No newline at end of file From d930344572b89f4c4fa3d1c8e94bd216fa70366e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 14:38:59 +0900 Subject: [PATCH 112/478] =?UTF-8?q?feat:=20(#35)=20AuthenticationErrorCode?= =?UTF-8?q?=EC=97=90=20=EC=BF=A0=ED=82=A4=EA=B0=92=EC=9D=B4=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/exception/AuthenticationErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index c6d6b16a8..7f604b9a5 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -24,7 +24,8 @@ public enum AuthenticationErrorCode implements BaseErrorCode { RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."), UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."), MISMATCH_TOKEN_MEMBER(HttpStatus.UNAUTHORIZED, "토큰의 회원 ID와 요청한 회원 ID가 일치하지 않습니다."), - NOT_EXIST_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "리프레시 토큰이 저장소에 존재하지 않습니다."); + NOT_EXIST_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "리프레시 토큰이 저장소에 존재하지 않습니다."), + MISSING_COOKIE_VALUE(HttpStatus.BAD_REQUEST, "쿠키값이 존재하지 않습니다."); private final HttpStatus httpStatus; From 1e2c06e013baba4fdc0025141c11431c78c263a3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 14:44:39 +0900 Subject: [PATCH 113/478] =?UTF-8?q?refactor:=20(#35)=20RefreshTokenRedisRe?= =?UTF-8?q?pository=EC=9D=98=20delete=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20deleteByMemberId=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/domain/repository/RefreshTokenRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index 5280d924d..508c5a42c 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -6,5 +6,5 @@ public interface RefreshTokenRepository { void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit); String findByMemberId(UUID memberId); - void delete(UUID memberId); + void deleteByMemberId(UUID memberId); } From d5f5dedb60f5231b4daad8033973a7fa3d6d4173 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 14:45:05 +0900 Subject: [PATCH 114/478] =?UTF-8?q?refactor:=20(#35)=20RotateTokenControll?= =?UTF-8?q?er=EB=A5=BC=20RotateAccessTokenController=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ateTokenController.java => RotateAccessTokenController.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/spring/backend/auth/presentation/{RotateTokenController.java => RotateAccessTokenController.java} (95%) diff --git a/src/main/java/spring/backend/auth/presentation/RotateTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java similarity index 95% rename from src/main/java/spring/backend/auth/presentation/RotateTokenController.java rename to src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 6ec4b2998..8dfb118e6 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -13,7 +13,7 @@ @RestController @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor -public class RotateTokenController { +public class RotateAccessTokenController { private final RotateTokenService rotateTokenService; @GetMapping From 5d77107627cdf1cabbe2964faf0f065454688da1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 14:48:47 +0900 Subject: [PATCH 115/478] =?UTF-8?q?feat:=20(#35)=20rotateAccessToken=20?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.=20-=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=EC=97=90=20refreshToken=EC=9D=B4=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=EC=8B=9C=ED=82=A8=EB=8B=A4.=20-=20=EC=BF=A0=ED=82=A4=EC=97=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EB=90=9C=20refreshToken=EA=B3=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=86=8C=EC=97=90=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=20refreshToken=EC=9D=B4=20=EC=9D=BC=EC=B9=98=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EB=A5=BC=20=EB=B0=9C=EC=83=9D=EC=8B=9C=ED=82=A8?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RotateTokenService.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RotateTokenService.java b/src/main/java/spring/backend/auth/application/RotateTokenService.java index f97f33ddd..01967890d 100644 --- a/src/main/java/spring/backend/auth/application/RotateTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateTokenService.java @@ -1,10 +1,10 @@ package spring.backend.auth.application; -import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import spring.backend.auth.dto.response.RotateTokenResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; import spring.backend.member.application.MemberService; @@ -16,9 +16,27 @@ public class RotateTokenService { private final MemberService memberService; private final JwtService jwtService; + private final RefreshTokenService refreshTokenService; public RotateTokenResponse rotateAccessToken(String refreshToken) { - UUID memberId = UUID.fromString(jwtService.getPayload(refreshToken).get("memberId", String.class)); + UUID memberId = extractMemberIdFromRefreshToken(refreshToken); + validateRefreshToken(memberId, refreshToken); return new RotateTokenResponse(jwtService.provideAccessToken(memberService.findByMemberId(memberId))); } + + private UUID extractMemberIdFromRefreshToken(String refreshToken) { + if(refreshToken == null) { + log.error("쿠키에 refreshToken이 존재하지 않습니다."); + throw AuthenticationErrorCode.MISSING_COOKIE_VALUE.toException(); + } + return UUID.fromString(jwtService.getPayload(refreshToken).get("memberId", String.class)); + } + + private void validateRefreshToken(UUID memberId, String refreshToken) { + String savedRefreshToken = refreshTokenService.getRefreshToken(memberId); + if (!savedRefreshToken.equals(refreshToken)) { + log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); + } + } } From c7d771a02aa91805fa270142995aa22a06e736d0 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 14:49:32 +0900 Subject: [PATCH 116/478] =?UTF-8?q?refactor:=20(#35)=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8D=98=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A5=BC=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=97=90=20=EC=9C=84=EC=9E=84=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 14 +++----- .../RefreshTokenRedisRepository.java | 32 +++++++++++++++---- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index b69ce6127..92ab105f6 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -30,10 +30,8 @@ public RefreshTokenService(JwtService jwtService, @Value("${jwt.refresh-token-ex public String saveRefreshToken(Member member) { try { - refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member),REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); + refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); return getRefreshToken(member.getId()); - } catch (RedisConnectionException e) { - throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); } catch (Exception e) { throw GlobalErrorCode.INTERNAL_ERROR.toException(); } @@ -41,14 +39,14 @@ public String saveRefreshToken(Member member) { public String getRefreshToken(UUID memberId) { try { - String refreshToken = refreshTokenRepository.findByMemberId(memberId); + String refreshToken = refreshTokenRepository.findByMemberId(memberId); + if (refreshToken == null || refreshToken.isEmpty()) { log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); } + return refreshToken; - } catch (RedisConnectionException e) { - throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); } catch (DomainException e) { throw e; } catch (Exception e) { @@ -58,9 +56,7 @@ public String getRefreshToken(UUID memberId) { public void deleteRefreshToken(UUID memberId) { try { - refreshTokenRepository.delete(memberId); - } catch (RedisConnectionException e) { - throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + refreshTokenRepository.deleteByMemberId(memberId); } catch (Exception e) { throw GlobalErrorCode.INTERNAL_ERROR.toException(); } diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index b37943f8a..eadf93bb8 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -1,33 +1,53 @@ package spring.backend.auth.infrastructure.redis.repository; +import io.lettuce.core.RedisConnectionException; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import spring.backend.auth.domain.repository.RefreshTokenRepository; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; import java.util.UUID; import java.util.concurrent.TimeUnit; @Repository @RequiredArgsConstructor +@Log4j2 public class RefreshTokenRedisRepository implements RefreshTokenRepository { private final RedisTemplate redisTemplate; @Override public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit) { - ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); + try { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); + } catch (RedisConnectionException e) { + log.error("Redis 연결 오류 : {}", e.getMessage()); + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } } @Override public String findByMemberId(UUID memberId) { - ValueOperations valueOperations = redisTemplate.opsForValue(); - return valueOperations.get(memberId.toString()); + try { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(memberId.toString()); + } catch (RedisConnectionException e) { + log.error("Redis 연결 오류 : {}", e.getMessage()); + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } } @Override - public void delete(UUID memberId) { - redisTemplate.delete(memberId.toString()); + public void deleteByMemberId(UUID memberId) { + try { + redisTemplate.delete(memberId.toString()); + } catch (RedisConnectionException e) { + log.error("Redis 연결 오류 : {}", e.getMessage()); + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } } } From cbb52901a554e8be13bb8ad8b300018b0942d207 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 15:13:47 +0900 Subject: [PATCH 117/478] =?UTF-8?q?refactor:=20(#35)=20RotateTokenService?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20RotateAccessTokenServ?= =?UTF-8?q?ice=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{RotateTokenService.java => RotateAccessTokenService.java} | 2 +- .../auth/presentation/RotateAccessTokenController.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/spring/backend/auth/application/{RotateTokenService.java => RotateAccessTokenService.java} (97%) diff --git a/src/main/java/spring/backend/auth/application/RotateTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java similarity index 97% rename from src/main/java/spring/backend/auth/application/RotateTokenService.java rename to src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index 01967890d..52841094c 100644 --- a/src/main/java/spring/backend/auth/application/RotateTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -13,7 +13,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class RotateTokenService { +public class RotateAccessTokenService { private final MemberService memberService; private final JwtService jwtService; private final RefreshTokenService refreshTokenService; diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 8dfb118e6..d2931d68b 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import spring.backend.auth.application.RotateTokenService; +import spring.backend.auth.application.RotateAccessTokenService; import spring.backend.auth.dto.response.RotateTokenResponse; import spring.backend.core.presentation.RestResponse; @@ -14,7 +14,7 @@ @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor public class RotateAccessTokenController { - private final RotateTokenService rotateTokenService; + private final RotateAccessTokenService rotateTokenService; @GetMapping public ResponseEntity> rotateAccessToken( From 08453ac52ef0916a14183f05cd5201275abe9df3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 15:14:16 +0900 Subject: [PATCH 118/478] =?UTF-8?q?feat:=20(#35)=20RotateAccessTokenServic?= =?UTF-8?q?e=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 1 - .../RefreshTokenRedisRepository.java | 1 - .../RotateAccessTokenServiceTest.java | 25 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 92ab105f6..13cabeb93 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -1,6 +1,5 @@ package spring.backend.auth.application; -import io.lettuce.core.RedisConnectionException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index eadf93bb8..97ff11c61 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -7,7 +7,6 @@ import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import spring.backend.auth.domain.repository.RefreshTokenRepository; -import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import java.util.UUID; diff --git a/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java new file mode 100644 index 000000000..114167f96 --- /dev/null +++ b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java @@ -0,0 +1,25 @@ +package spring.backend.core.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.auth.application.RotateAccessTokenService; +import spring.backend.core.exception.DomainException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +@SpringBootTest +public class RotateAccessTokenServiceTest { + @Autowired + private RotateAccessTokenService rotateAccessTokenService; + + @DisplayName("Cookie에 refreshToken이 존재하지 않는 경우 예외를 발생시킨다.") + @Test + void throwExceptionWhenRefreshTokenNotExistsInCookie() { + // when, then + assertThatThrownBy(() -> rotateAccessTokenService.rotateAccessToken(null)) + .isInstanceOf(DomainException.class) + .hasMessage("쿠키값이 존재하지 않습니다."); + } +} From 77ccad2cedb77a9c1830f18521197fbe2e36e2b7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 22:52:37 +0900 Subject: [PATCH 119/478] =?UTF-8?q?refactor:=20(#35)=20RefreshTokenService?= =?UTF-8?q?=EC=9D=98=20getRefreshToken=EC=97=90=EC=84=9C=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EB=8A=94=20=EC=A4=91=20=EC=9D=BC=EB=B6=80?= =?UTF-8?q?=EB=A5=BC=20RefreshTokenRedisRepository=EB=A1=9C=20=EC=9C=84?= =?UTF-8?q?=EC=9E=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/RefreshTokenService.java | 9 +-------- .../redis/repository/RefreshTokenRedisRepository.java | 8 +++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 13cabeb93..bbd39dc77 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -38,14 +38,7 @@ public String saveRefreshToken(Member member) { public String getRefreshToken(UUID memberId) { try { - String refreshToken = refreshTokenRepository.findByMemberId(memberId); - - if (refreshToken == null || refreshToken.isEmpty()) { - log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); - throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); - } - - return refreshToken; + return refreshTokenRepository.findByMemberId(memberId); } catch (DomainException e) { throw e; } catch (Exception e) { diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index 97ff11c61..354a5512d 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import spring.backend.auth.domain.repository.RefreshTokenRepository; +import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import java.util.UUID; @@ -33,7 +34,12 @@ public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit t public String findByMemberId(UUID memberId) { try { ValueOperations valueOperations = redisTemplate.opsForValue(); - return valueOperations.get(memberId.toString()); + String refreshToken = valueOperations.get(memberId.toString()); + if (refreshToken == null || refreshToken.isEmpty()) { + log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); + } + return refreshToken; } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); From 0c6323c37580aba918e0d0feadeabb801e39019d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 22:54:47 +0900 Subject: [PATCH 120/478] =?UTF-8?q?refactor:=20(#35)=20RotateTokenResponse?= =?UTF-8?q?=EB=A5=BC=20RotateAccessTokenResponse=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RotateAccessTokenService.java | 8 ++++---- .../auth/dto/response/RotateAccessTokenResponse.java | 4 ++++ .../backend/auth/dto/response/RotateTokenResponse.java | 4 ---- .../auth/presentation/RotateAccessTokenController.java | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java delete mode 100644 src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index 52841094c..f19b35b29 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import spring.backend.auth.dto.response.RotateTokenResponse; +import spring.backend.auth.dto.response.RotateAccessTokenResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; import spring.backend.member.application.MemberService; @@ -18,14 +18,14 @@ public class RotateAccessTokenService { private final JwtService jwtService; private final RefreshTokenService refreshTokenService; - public RotateTokenResponse rotateAccessToken(String refreshToken) { + public RotateAccessTokenResponse rotateAccessToken(String refreshToken) { UUID memberId = extractMemberIdFromRefreshToken(refreshToken); validateRefreshToken(memberId, refreshToken); - return new RotateTokenResponse(jwtService.provideAccessToken(memberService.findByMemberId(memberId))); + return new RotateAccessTokenResponse(jwtService.provideAccessToken(memberService.findByMemberId(memberId))); } private UUID extractMemberIdFromRefreshToken(String refreshToken) { - if(refreshToken == null) { + if (refreshToken == null) { log.error("쿠키에 refreshToken이 존재하지 않습니다."); throw AuthenticationErrorCode.MISSING_COOKIE_VALUE.toException(); } diff --git a/src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java b/src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java new file mode 100644 index 000000000..949638034 --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java @@ -0,0 +1,4 @@ +package spring.backend.auth.dto.response; + +public record RotateAccessTokenResponse(String accessToken) { +} diff --git a/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java b/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java deleted file mode 100644 index fc76d7ea7..000000000 --- a/src/main/java/spring/backend/auth/dto/response/RotateTokenResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package spring.backend.auth.dto.response; - -public record RotateTokenResponse(String accessToken) { -} diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index d2931d68b..600914b38 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; -import spring.backend.auth.dto.response.RotateTokenResponse; +import spring.backend.auth.dto.response.RotateAccessTokenResponse; import spring.backend.core.presentation.RestResponse; @RestController @@ -17,7 +17,7 @@ public class RotateAccessTokenController { private final RotateAccessTokenService rotateTokenService; @GetMapping - public ResponseEntity> rotateAccessToken( + public ResponseEntity> rotateAccessToken( @CookieValue(name = "refreshToken", required = false) String refreshToken ) { return ResponseEntity.ok(new RestResponse<>(rotateTokenService.rotateAccessToken(refreshToken))); From a4dc26928b63c74be9edb235430ca312842b0e5a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Oct 2024 23:01:25 +0900 Subject: [PATCH 121/478] =?UTF-8?q?feat:=20(#35)=20RefreshTokenRedisReposi?= =?UTF-8?q?toryTest=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=98=EA=B3=A0=20Refr?= =?UTF-8?q?eshTokenService=EC=9D=98=20=EC=97=90=EB=9F=AC=EB=A5=BC=20Reposi?= =?UTF-8?q?tory=EB=A1=9C=20=EC=9C=84=EC=9E=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 29 +++++++------------ .../RefreshTokenRedisRepository.java | 15 +++++----- .../RefreshTokenRedisRepositoryTest.java | 28 ++++++++++++++++++ 3 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index bbd39dc77..765ecd48b 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -6,8 +6,6 @@ import spring.backend.auth.domain.repository.RefreshTokenRepository; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; -import spring.backend.core.exception.DomainException; -import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.member.domain.entity.Member; import java.time.temporal.ChronoUnit; @@ -28,30 +26,23 @@ public RefreshTokenService(JwtService jwtService, @Value("${jwt.refresh-token-ex } public String saveRefreshToken(Member member) { - try { - refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); - return getRefreshToken(member.getId()); - } catch (Exception e) { - throw GlobalErrorCode.INTERNAL_ERROR.toException(); - } + refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); + return getRefreshToken(member.getId()); } public String getRefreshToken(UUID memberId) { - try { - return refreshTokenRepository.findByMemberId(memberId); - } catch (DomainException e) { - throw e; - } catch (Exception e) { - throw GlobalErrorCode.INTERNAL_ERROR.toException(); + String refreshToken = refreshTokenRepository.findByMemberId(memberId); + + if (refreshToken == null || refreshToken.isEmpty()) { + log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); } + + return refreshToken; } public void deleteRefreshToken(UUID memberId) { - try { - refreshTokenRepository.deleteByMemberId(memberId); - } catch (Exception e) { - throw GlobalErrorCode.INTERNAL_ERROR.toException(); - } + refreshTokenRepository.deleteByMemberId(memberId); } private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index 354a5512d..77dce1cc4 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -7,7 +7,6 @@ import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Repository; import spring.backend.auth.domain.repository.RefreshTokenRepository; -import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import java.util.UUID; @@ -27,6 +26,8 @@ public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit t } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } @@ -34,16 +35,14 @@ public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit t public String findByMemberId(UUID memberId) { try { ValueOperations valueOperations = redisTemplate.opsForValue(); - String refreshToken = valueOperations.get(memberId.toString()); - if (refreshToken == null || refreshToken.isEmpty()) { - log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); - throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); - } - return refreshToken; + return valueOperations.get(memberId.toString()); } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); } + } @Override @@ -53,6 +52,8 @@ public void deleteByMemberId(UUID memberId) { } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } } diff --git a/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java b/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java new file mode 100644 index 000000000..2b80b1e74 --- /dev/null +++ b/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java @@ -0,0 +1,28 @@ +package spring.backend.core.infrastructure; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; + +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +@SpringBootTest +public class RefreshTokenRedisRepositoryTest { + @Autowired + private RefreshTokenRedisRepository refreshTokenRedisRepository; + + @DisplayName("인자로 받은 memberId에 해당하는 refreshToken이 Redis에 존재하지 않을 때 예외를 던진다.") + @Test + public void throwExceptionWhenRefreshTokenSingedByMemberIdIsNotExistsInRedis() { + // GIVEN + UUID memberId = UUID.randomUUID(); + // WHEN & THEN + assertThatThrownBy(() -> refreshTokenRedisRepository.findByMemberId(memberId)) + .isInstanceOf(RuntimeException.class) + .hasMessage("리프레시 토큰이 저장소에 존재하지 않습니다."); + } +} From ef646bd5eb4beb383e0285b2c5d12ccd2c55fbab Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Oct 2024 20:54:34 +0900 Subject: [PATCH 122/478] =?UTF-8?q?fix:=20(#53)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RefreshTokenRedisRepositoryTest.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java diff --git a/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java b/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java deleted file mode 100644 index 2b80b1e74..000000000 --- a/src/test/java/spring/backend/core/infrastructure/RefreshTokenRedisRepositoryTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package spring.backend.core.infrastructure; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; - -import java.util.UUID; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -@SpringBootTest -public class RefreshTokenRedisRepositoryTest { - @Autowired - private RefreshTokenRedisRepository refreshTokenRedisRepository; - - @DisplayName("인자로 받은 memberId에 해당하는 refreshToken이 Redis에 존재하지 않을 때 예외를 던진다.") - @Test - public void throwExceptionWhenRefreshTokenSingedByMemberIdIsNotExistsInRedis() { - // GIVEN - UUID memberId = UUID.randomUUID(); - // WHEN & THEN - assertThatThrownBy(() -> refreshTokenRedisRepository.findByMemberId(memberId)) - .isInstanceOf(RuntimeException.class) - .hasMessage("리프레시 토큰이 저장소에 존재하지 않습니다."); - } -} From 7dbc489dd1d808616c9386f7b9cb8c579d845ced Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 04:56:18 +0900 Subject: [PATCH 123/478] =?UTF-8?q?fix:=20(#34)=20Member=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EB=AA=A8=EB=8D=B8=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EB=B3=80=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Member.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 1ce438b97..949319bcb 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -13,21 +13,21 @@ @Builder public class Member { - private final UUID id; + private UUID id; - private final Provider provider; + private Provider provider; - private final Role role; + private Role role; - private final String email; + private String email; - private final String nickname; + private String nickname; - private final LocalDateTime createdAt; + private LocalDateTime createdAt; - private final LocalDateTime updatedAt; + private LocalDateTime updatedAt; - private final Boolean deleted; + private Boolean deleted; public static Member toDomainEntity(MemberJpaEntity memberJpaEntity) { return Member.builder() From a023002b89bfc3932b7ae041914dc8d3b279b80a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 07:18:06 +0900 Subject: [PATCH 124/478] =?UTF-8?q?feat:=20(#34)=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20=EC=B6=9C=EC=83=9D?= =?UTF-8?q?=EB=85=84=EB=8F=84,=20=EC=84=B1=EB=B3=84,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/member/domain/entity/Member.java | 10 ++++++++++ .../spring/backend/member/domain/value/Gender.java | 11 +++++++++++ .../persistence/jpa/entity/MemberJpaEntity.java | 11 +++++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/main/java/spring/backend/member/domain/value/Gender.java diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 949319bcb..4e65a4d3c 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Getter; +import spring.backend.member.domain.value.Gender; import spring.backend.member.domain.value.Provider; import spring.backend.member.domain.value.Role; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; @@ -23,6 +24,12 @@ public class Member { private String nickname; + private int birthYear; + + private Gender gender; + + private String profileImage; + private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -36,6 +43,9 @@ public static Member toDomainEntity(MemberJpaEntity memberJpaEntity) { .role(memberJpaEntity.getRole()) .email(memberJpaEntity.getEmail()) .nickname(memberJpaEntity.getNickname()) + .birthYear(memberJpaEntity.getBirthYear()) + .gender(memberJpaEntity.getGender()) + .profileImage(memberJpaEntity.getProfileImage()) .createdAt(memberJpaEntity.getCreatedAt()) .updatedAt(memberJpaEntity.getUpdatedAt()) .deleted(memberJpaEntity.getDeleted()) diff --git a/src/main/java/spring/backend/member/domain/value/Gender.java b/src/main/java/spring/backend/member/domain/value/Gender.java new file mode 100644 index 000000000..7154d7c7b --- /dev/null +++ b/src/main/java/spring/backend/member/domain/value/Gender.java @@ -0,0 +1,11 @@ +package spring.backend.member.domain.value; + +import lombok.Getter; + +@Getter +public enum Gender { + + MALE, + FEMALE, + NONE +} diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java index ff0db6b17..5a6254b20 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java @@ -10,6 +10,7 @@ import lombok.experimental.SuperBuilder; import spring.backend.core.infrastructure.jpa.shared.BaseEntity; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Gender; import spring.backend.member.domain.value.Provider; import spring.backend.member.domain.value.Role; @@ -32,6 +33,13 @@ public class MemberJpaEntity extends BaseEntity { private String nickname; + private int birthYear; + + @Enumerated(EnumType.STRING) + private Gender gender; + + private String profileImage; + public static MemberJpaEntity toJpaEntity(Member member) { return MemberJpaEntity.builder() .id(member.getId()) @@ -39,6 +47,9 @@ public static MemberJpaEntity toJpaEntity(Member member) { .role(member.getRole()) .email(member.getEmail()) .nickname(member.getNickname()) + .birthYear(member.getBirthYear()) + .gender(member.getGender()) + .profileImage(member.getProfileImage()) .createdAt(member.getCreatedAt()) .updatedAt(member.getUpdatedAt()) .deleted(Optional.ofNullable(member.getDeleted()).orElse(false)) From 1970f7e50471d94d313e934bc4df698e080133fe Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 07:22:07 +0900 Subject: [PATCH 125/478] =?UTF-8?q?feat:=20(#34)=20=EA=B2=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=ED=9A=8C=EC=9B=90=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=ED=95=98=EB=8A=94=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/member/domain/entity/Member.java | 12 ++++++++++++ .../backend/member/exception/MemberErrorCode.java | 1 + 2 files changed, 13 insertions(+) diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 4e65a4d3c..880adcb56 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -5,6 +5,7 @@ import spring.backend.member.domain.value.Gender; import spring.backend.member.domain.value.Provider; import spring.backend.member.domain.value.Role; +import spring.backend.member.exception.MemberErrorCode; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; import java.time.LocalDateTime; @@ -60,6 +61,17 @@ public boolean isMember() { return Role.MEMBER.equals(this.role); } + public void convertGuestToMember(String nickname, int birthYear, Gender gender, String profileImage) { + if (isMember()) { + throw MemberErrorCode.ALREADY_REGISTERED_MEMBER.toException(); + } + this.role = Role.MEMBER; + this.nickname = nickname; + this.birthYear = birthYear; + this.gender = gender; + this.profileImage = profileImage; + } + public static Member createGuestMember(Provider provider, String email, String nickname) { return Member.builder() .provider(provider) diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index ff8ca0bea..83e09765e 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -14,6 +14,7 @@ public enum MemberErrorCode implements BaseErrorCode { ALREADY_REGISTERED_WITH_DIFFERENT_OAUTH2(HttpStatus.BAD_REQUEST, "이미 다른 소셜 로그인으로 가입된 계정입니다."), MEMBER_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "사용자 정보를 저장하는데 실패하였습니다."), NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."), + ALREADY_REGISTERED_MEMBER(HttpStatus.BAD_REQUEST, "이미 가입된 사용자입니다."), NOT_EXIST_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임은 필수 입력값입니다."), INVALID_NICKNAME_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 1자에서 6자 사이여야 합니다."), INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영문, 숫자 조합이어야 합니다."), From f17d51460d78eb2a21012b866a968ed76bd16d47 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 18:53:48 +0900 Subject: [PATCH 126/478] =?UTF-8?q?refactor:=20(#34)=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=A0=9C=EA=B3=B5=ED=95=98?= =?UTF-8?q?=EB=8A=94=20MemberService=EB=A5=BC=20=20MemberServiceHelper?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=85=EB=AA=85=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../argumentresolver/LoginMemberArgumentResolver.java | 6 +++--- .../{MemberService.java => MemberServiceHelper.java} | 2 +- .../argumentresolver/LoginMemberArgumentResolverTest.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/spring/backend/member/application/{MemberService.java => MemberServiceHelper.java} (95%) diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java index 729633f62..1c97b7331 100644 --- a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java @@ -10,7 +10,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; -import spring.backend.member.application.MemberService; +import spring.backend.member.application.MemberServiceHelper; import java.util.UUID; @@ -25,7 +25,7 @@ public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolve private final JwtService jwtService; - private final MemberService memberService; + private final MemberServiceHelper memberServiceHelper; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -37,7 +37,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m String authorizationHeader = webRequest.getHeader(AUTHORIZATION_HEADER); String token = extractToken(authorizationHeader); UUID memberId = jwtService.extractMemberId(token); - return memberService.findByMemberId(memberId); + return memberServiceHelper.findByMemberId(memberId); } private String extractToken(String authorizationHeader) { diff --git a/src/main/java/spring/backend/member/application/MemberService.java b/src/main/java/spring/backend/member/application/MemberServiceHelper.java similarity index 95% rename from src/main/java/spring/backend/member/application/MemberService.java rename to src/main/java/spring/backend/member/application/MemberServiceHelper.java index 7abe62136..8b19146be 100644 --- a/src/main/java/spring/backend/member/application/MemberService.java +++ b/src/main/java/spring/backend/member/application/MemberServiceHelper.java @@ -11,7 +11,7 @@ @Service @RequiredArgsConstructor -public class MemberService { +public class MemberServiceHelper { private final MemberRepository memberRepository; diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java index d03a5bc79..1124333ee 100644 --- a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java @@ -11,7 +11,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import spring.backend.core.application.JwtService; -import spring.backend.member.application.MemberService; +import spring.backend.member.application.MemberServiceHelper; import spring.backend.member.domain.entity.Member; import java.util.UUID; @@ -31,7 +31,7 @@ public class LoginMemberArgumentResolverTest { private JwtService jwtService; @Mock - private MemberService memberService; + private MemberServiceHelper memberServiceHelper; @Mock private NativeWebRequest webRequest; @@ -69,7 +69,7 @@ public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Excep when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); - when(memberService.findByMemberId(memberId)).thenReturn(member); + when(memberServiceHelper.findByMemberId(memberId)).thenReturn(member); // then Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); From 33d39ff44da3ae9e43e62cbd5e6c6b89300d1c7a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 21 Oct 2024 07:23:28 +0900 Subject: [PATCH 127/478] =?UTF-8?q?feat:=20(#34)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20API=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/OnboardingSignUpService.java | 54 +++++++++++++++++++ .../dto/request/OnboardingSignUpRequest.java | 20 +++++++ .../exception/AuthenticationErrorCode.java | 6 ++- .../OnboardingSignUpController.java | 30 +++++++++++ .../application/MemberServiceHelper.java | 4 ++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/main/java/spring/backend/auth/application/OnboardingSignUpService.java create mode 100644 src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java create mode 100644 src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java diff --git a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java new file mode 100644 index 000000000..a370e8f42 --- /dev/null +++ b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java @@ -0,0 +1,54 @@ +package spring.backend.auth.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.member.application.MemberServiceHelper; +import spring.backend.member.domain.entity.Member; + +import java.time.Year; + +@Service +@RequiredArgsConstructor +@Transactional +@Log4j2 +public class OnboardingSignUpService { + + private static final int BIRTH_YEAR_RANGE = 100; + + private final MemberServiceHelper memberServiceHelper; + + public Member onboardingSignUp(Member member, OnboardingSignUpRequest request) { + validateMember(member); + validateRequest(request); + validateBirthYear(request); + member.convertGuestToMember(request.nickname(), request.birthYear(), request.gender(), request.profileImage()); + return memberServiceHelper.save(member); + } + + private void validateMember(Member member) { + if (member == null || member.isMember()) { + log.error("[OnboardingSignUpService] Invalid member condition for sign-up."); + throw AuthenticationErrorCode.INVALID_MEMBER_SIGN_UP_CONDITION.toException(); + } + } + + private void validateRequest(OnboardingSignUpRequest request) { + if (request == null) { + log.error("[OnboardingSignUpService] Invalid request."); + throw AuthenticationErrorCode.NOT_EXIST_SIGN_UP_CONDITION.toException(); + } + } + + private void validateBirthYear(OnboardingSignUpRequest request) { + int birthYear = request.birthYear(); + int currentYear = Year.now().getValue(); + if (birthYear < (currentYear - BIRTH_YEAR_RANGE) || birthYear > currentYear) { + log.error("[OnboardingSignUpService] Invalid request birth year."); + throw AuthenticationErrorCode.INVALID_BIRTH_YEAR.toException(); + } + } +} diff --git a/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java b/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java new file mode 100644 index 000000000..a8c6e850f --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java @@ -0,0 +1,20 @@ +package spring.backend.auth.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import spring.backend.member.domain.value.Gender; + +public record OnboardingSignUpRequest( + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,6}$", message = "닉네임은 한글, 영문, 숫자 조합 6자 이내로 입력해주세요.") + String nickname, + + int birthYear, + + @NotNull(message = "성별을 입력해주세요.") + Gender gender, + + @NotBlank(message = "프로필 이미지를 선택해주세요.") + String profileImage +) { +} diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 7f604b9a5..19fc8aa6d 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -25,8 +25,10 @@ public enum AuthenticationErrorCode implements BaseErrorCode { UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."), MISMATCH_TOKEN_MEMBER(HttpStatus.UNAUTHORIZED, "토큰의 회원 ID와 요청한 회원 ID가 일치하지 않습니다."), NOT_EXIST_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "리프레시 토큰이 저장소에 존재하지 않습니다."), - MISSING_COOKIE_VALUE(HttpStatus.BAD_REQUEST, "쿠키값이 존재하지 않습니다."); - + MISSING_COOKIE_VALUE(HttpStatus.BAD_REQUEST, "쿠키값이 존재하지 않습니다."), + INVALID_MEMBER_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입을 위한 사용자 조건이 유효하지 않습니다."), + NOT_EXIST_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입 요청이 유효하지 않습니다."), + INVALID_BIRTH_YEAR(HttpStatus.BAD_REQUEST, "출생년도는 현재 연도와 100년 전 사이여야 합니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java new file mode 100644 index 000000000..22aff7792 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java @@ -0,0 +1,30 @@ +package spring.backend.auth.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.auth.application.OnboardingSignUpService; +import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +public class OnboardingSignUpController { + + private final OnboardingSignUpService onboardingSignUpService; + + @Authorization + @PostMapping("/v1/members/onboard") + public ResponseEntity> onboardingSignUp(@LoginMember Member member, @Valid @RequestBody OnboardingSignUpRequest request) { + Member updatedMember = onboardingSignUpService.onboardingSignUp(member, request); + return ResponseEntity.ok(new RestResponse<>(updatedMember.getId())); + } +} diff --git a/src/main/java/spring/backend/member/application/MemberServiceHelper.java b/src/main/java/spring/backend/member/application/MemberServiceHelper.java index 8b19146be..e81dd83fd 100644 --- a/src/main/java/spring/backend/member/application/MemberServiceHelper.java +++ b/src/main/java/spring/backend/member/application/MemberServiceHelper.java @@ -19,4 +19,8 @@ public Member findByMemberId(UUID memberId) { Member member = memberRepository.findById(memberId); return Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException); } + + public Member save(Member member) { + return memberRepository.save(member); + } } From 9403f737fc8bfd0f3c1074251dc673660a39e87b Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 22 Oct 2024 18:41:25 +0900 Subject: [PATCH 128/478] =?UTF-8?q?feat:=20(#34)=20Spring=20Validation?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A5=BC=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=B2=98=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/GlobalExceptionHandler.java | 13 ++++++++++++- .../backend/core/presentation/ErrorResponse.java | 12 +++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java index ee1b80237..0129afb16 100644 --- a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java +++ b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java @@ -1,15 +1,19 @@ package spring.backend.core.exception; -import java.util.Optional; import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import spring.backend.core.presentation.ErrorResponse; +import java.util.Optional; + @RestControllerAdvice @Log4j2 public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @@ -28,4 +32,11 @@ public final ResponseEntity handleDomainException(DomainException ErrorResponse errorResponse = ErrorResponse.createDomainErrorResponse().statusCode(httpStatus.value()).exception(ex).build(); return ResponseEntity.status(httpStatus).body(errorResponse); } + + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + log.error("ERROR ::: [MethodArgumentNotValidException] ", ex); + ErrorResponse errorResponse = ErrorResponse.createValidationErrorResponse().statusCode(400).exception(ex).build(); + return ResponseEntity.badRequest().body(errorResponse); + } } diff --git a/src/main/java/spring/backend/core/presentation/ErrorResponse.java b/src/main/java/spring/backend/core/presentation/ErrorResponse.java index d8065e20e..404801b17 100644 --- a/src/main/java/spring/backend/core/presentation/ErrorResponse.java +++ b/src/main/java/spring/backend/core/presentation/ErrorResponse.java @@ -1,11 +1,13 @@ package spring.backend.core.presentation; -import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; +import org.springframework.web.bind.MethodArgumentNotValidException; import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.BaseErrorCode; +import java.time.LocalDateTime; + @Getter public class ErrorResponse extends BaseResponse { @@ -38,4 +40,12 @@ public ErrorResponse(BaseErrorCode baseErrorCode) { this.code = baseErrorCode.name(); this.message = baseErrorCode.getMessage(); } + + @Builder(builderClassName = "CreateValidationErrorResponse", builderMethodName = "createValidationErrorResponse") + public ErrorResponse(int statusCode, MethodArgumentNotValidException exception) { + super(false, LocalDateTime.now()); + this.statusCode = statusCode; + this.code = exception.getClass().getSimpleName(); + this.message = exception.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); + } } From 22ae12d8b3c9d062da04cfb72cc358c700a57244 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 22 Oct 2024 18:42:30 +0900 Subject: [PATCH 129/478] =?UTF-8?q?feat:=20(#34)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EA=B0=80=EC=9E=85=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/OnboardingSignUpRequest.java | 10 ++++--- .../OnboardingSignUpController.java | 3 ++- .../swagger/OnboardingSignUpSwagger.java | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java diff --git a/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java b/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java index a8c6e850f..033f2535b 100644 --- a/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java +++ b/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java @@ -1,20 +1,24 @@ package spring.backend.auth.dto.request; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; import spring.backend.member.domain.value.Gender; public record OnboardingSignUpRequest( + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,6}$", message = "닉네임은 한글, 영문, 숫자 조합 6자 이내로 입력해주세요.") + @Schema(description = "닉네임", example = "조각조각") String nickname, + @Schema(description = "출생년도", example = "2001") int birthYear, @NotNull(message = "성별을 입력해주세요.") + @Schema(description = "성별 (MALE, FEMALE, NONE)", example = "FEMALE") Gender gender, @NotBlank(message = "프로필 이미지를 선택해주세요.") + @Schema(description = "프로필 이미지", example = "http://test.jpg") String profileImage ) { } diff --git a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java index 22aff7792..52d0541b3 100644 --- a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java +++ b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.OnboardingSignUpService; import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.presentation.swagger.OnboardingSignUpSwagger; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; @@ -17,7 +18,7 @@ @RestController @RequiredArgsConstructor -public class OnboardingSignUpController { +public class OnboardingSignUpController implements OnboardingSignUpSwagger { private final OnboardingSignUpService onboardingSignUpService; diff --git a/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java new file mode 100644 index 000000000..676ce67b2 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java @@ -0,0 +1,27 @@ +package spring.backend.auth.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.exception.MemberErrorCode; + +import java.util.UUID; + +@Tag(name = "Auth", description = "인증/인가") +public interface OnboardingSignUpSwagger { + + @Operation( + summary = "가입 온보딩 API", + description = "사용자가 닉네임, 나이, 성별, 프로필사진을 입력하여 회원가입을 진행합니다", + operationId = "/v1/members/onboard" + ) + @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class, MemberErrorCode.class}) + ResponseEntity> onboardingSignUp(@Parameter(hidden = true) Member member, OnboardingSignUpRequest request); +} From f15ab59b6a3b842fb2779b4e681fb0396b8d9c5c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 22 Oct 2024 18:57:15 +0900 Subject: [PATCH 130/478] =?UTF-8?q?feat:=20(#34)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EA=B0=80=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingSignUpServiceTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java diff --git a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java new file mode 100644 index 000000000..f1ef48fc8 --- /dev/null +++ b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java @@ -0,0 +1,86 @@ +package spring.backend.auth.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.exception.DomainException; +import spring.backend.member.application.MemberServiceHelper; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Gender; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class OnboardingSignUpServiceTest { + + @InjectMocks + private OnboardingSignUpService onboardingSignUpService; + + @Mock + private MemberServiceHelper memberServiceHelper; + + private Member member; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + member = mock(Member.class); + } + + @DisplayName("회원가입 요청이 null인 경우 예외가 발생한다") + @Test + public void throwExceptionWhenRequestIsNull() { + // When & Then + Exception exception = assertThrows(DomainException.class, + () -> onboardingSignUpService.onboardingSignUp(member, null)); + assertEquals(AuthenticationErrorCode.NOT_EXIST_SIGN_UP_CONDITION.getMessage(), exception.getMessage()); + } + + @DisplayName("유효하지 않은 멤버 상태인 경우 예외가 발생한다") + @Test + public void throwExceptionWhenMemberIsInvalid() { + // Given + when(member.isMember()).thenReturn(true); + + // When & Then + Exception exception = assertThrows(DomainException.class, + () -> onboardingSignUpService.onboardingSignUp(member, new OnboardingSignUpRequest("조각조각", 2000, Gender.MALE, "http://test.jpg"))); + assertEquals(AuthenticationErrorCode.INVALID_MEMBER_SIGN_UP_CONDITION.getMessage(), exception.getMessage()); + } + + @DisplayName("출생년도가 유효하지 않은 경우 예외가 발생한다") + @Test + public void throwExceptionWhenBirthYearIsInvalid() { + // Given + OnboardingSignUpRequest request = new OnboardingSignUpRequest("조각조각", 1900, Gender.MALE, "http://test.jpg"); + + // When & Then + Exception exception = assertThrows(DomainException.class, + () -> onboardingSignUpService.onboardingSignUp(member, request)); + + assertEquals(AuthenticationErrorCode.INVALID_BIRTH_YEAR.getMessage(), exception.getMessage()); + } + + @DisplayName("유효한 회원가입 요청인 경우 회원 정보가 저장된다") + @Test + public void saveMemberWhenRequestIsValid() { + // Given + OnboardingSignUpRequest request = new OnboardingSignUpRequest("조각조각", 2001, Gender.MALE, "http://test.jpg"); + when(member.isMember()).thenReturn(false); + when(memberServiceHelper.save(any(Member.class))).thenReturn(member); + + // When + Member result = onboardingSignUpService.onboardingSignUp(member, request); + + // Then + assertNotNull(result); + verify(member).convertGuestToMember("조각조각", 2001, Gender.MALE, "http://test.jpg"); + verify(memberServiceHelper).save(member); + } +} \ No newline at end of file From 4dd9836686d582febdb87a36c02cfef915a87f30 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 24 Oct 2024 00:54:50 +0900 Subject: [PATCH 131/478] =?UTF-8?q?fix:=20(#34)=20Member=20=ED=97=AC?= =?UTF-8?q?=ED=8D=BC=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20MemberReposi?= =?UTF-8?q?tory=EB=A5=BC=20=EC=9D=98=EC=A1=B4=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/OnboardingSignUpService.java | 6 ++--- .../application/RotateAccessTokenService.java | 10 ++++--- .../LoginMemberArgumentResolver.java | 10 ++++--- .../application/MemberServiceHelper.java | 26 ------------------- .../OnboardingSignUpServiceTest.java | 8 +++--- .../LoginMemberArgumentResolverTest.java | 6 ++--- 6 files changed, 24 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/spring/backend/member/application/MemberServiceHelper.java diff --git a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java index a370e8f42..d4fff3705 100644 --- a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java +++ b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java @@ -6,8 +6,8 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.auth.dto.request.OnboardingSignUpRequest; import spring.backend.auth.exception.AuthenticationErrorCode; -import spring.backend.member.application.MemberServiceHelper; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; import java.time.Year; @@ -19,14 +19,14 @@ public class OnboardingSignUpService { private static final int BIRTH_YEAR_RANGE = 100; - private final MemberServiceHelper memberServiceHelper; + private final MemberRepository memberRepository; public Member onboardingSignUp(Member member, OnboardingSignUpRequest request) { validateMember(member); validateRequest(request); validateBirthYear(request); member.convertGuestToMember(request.nickname(), request.birthYear(), request.gender(), request.profileImage()); - return memberServiceHelper.save(member); + return memberRepository.save(member); } private void validateMember(Member member) { diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index f19b35b29..769ad2b02 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -6,22 +6,26 @@ import spring.backend.auth.dto.response.RotateAccessTokenResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; -import spring.backend.member.application.MemberService; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.exception.MemberErrorCode; +import java.util.Optional; import java.util.UUID; @Service @RequiredArgsConstructor @Slf4j public class RotateAccessTokenService { - private final MemberService memberService; + private final MemberRepository memberRepository; private final JwtService jwtService; private final RefreshTokenService refreshTokenService; public RotateAccessTokenResponse rotateAccessToken(String refreshToken) { UUID memberId = extractMemberIdFromRefreshToken(refreshToken); validateRefreshToken(memberId, refreshToken); - return new RotateAccessTokenResponse(jwtService.provideAccessToken(memberService.findByMemberId(memberId))); + Member member = memberRepository.findById(memberId); + return new RotateAccessTokenResponse(jwtService.provideAccessToken(Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException))); } private UUID extractMemberIdFromRefreshToken(String refreshToken) { diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java index 1c97b7331..d91640cd8 100644 --- a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java @@ -10,8 +10,11 @@ import org.springframework.web.method.support.ModelAndViewContainer; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; -import spring.backend.member.application.MemberServiceHelper; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.exception.MemberErrorCode; +import java.util.Optional; import java.util.UUID; @Component @@ -25,7 +28,7 @@ public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolve private final JwtService jwtService; - private final MemberServiceHelper memberServiceHelper; + private final MemberRepository memberRepository; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -37,7 +40,8 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m String authorizationHeader = webRequest.getHeader(AUTHORIZATION_HEADER); String token = extractToken(authorizationHeader); UUID memberId = jwtService.extractMemberId(token); - return memberServiceHelper.findByMemberId(memberId); + Member member = memberRepository.findById(memberId); + return Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException); } private String extractToken(String authorizationHeader) { diff --git a/src/main/java/spring/backend/member/application/MemberServiceHelper.java b/src/main/java/spring/backend/member/application/MemberServiceHelper.java deleted file mode 100644 index e81dd83fd..000000000 --- a/src/main/java/spring/backend/member/application/MemberServiceHelper.java +++ /dev/null @@ -1,26 +0,0 @@ -package spring.backend.member.application; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import spring.backend.member.domain.entity.Member; -import spring.backend.member.domain.repository.MemberRepository; -import spring.backend.member.exception.MemberErrorCode; - -import java.util.Optional; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class MemberServiceHelper { - - private final MemberRepository memberRepository; - - public Member findByMemberId(UUID memberId) { - Member member = memberRepository.findById(memberId); - return Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException); - } - - public Member save(Member member) { - return memberRepository.save(member); - } -} diff --git a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java index f1ef48fc8..a85ca4705 100644 --- a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java +++ b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java @@ -9,8 +9,8 @@ import spring.backend.auth.dto.request.OnboardingSignUpRequest; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.DomainException; -import spring.backend.member.application.MemberServiceHelper; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.domain.value.Gender; import static org.junit.jupiter.api.Assertions.*; @@ -23,7 +23,7 @@ class OnboardingSignUpServiceTest { private OnboardingSignUpService onboardingSignUpService; @Mock - private MemberServiceHelper memberServiceHelper; + private MemberRepository memberRepository; private Member member; @@ -73,7 +73,7 @@ public void saveMemberWhenRequestIsValid() { // Given OnboardingSignUpRequest request = new OnboardingSignUpRequest("조각조각", 2001, Gender.MALE, "http://test.jpg"); when(member.isMember()).thenReturn(false); - when(memberServiceHelper.save(any(Member.class))).thenReturn(member); + when(memberRepository.save(any(Member.class))).thenReturn(member); // When Member result = onboardingSignUpService.onboardingSignUp(member, request); @@ -81,6 +81,6 @@ public void saveMemberWhenRequestIsValid() { // Then assertNotNull(result); verify(member).convertGuestToMember("조각조각", 2001, Gender.MALE, "http://test.jpg"); - verify(memberServiceHelper).save(member); + verify(memberRepository).save(member); } } \ No newline at end of file diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java index 1124333ee..23fc9681f 100644 --- a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java @@ -11,8 +11,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import spring.backend.core.application.JwtService; -import spring.backend.member.application.MemberServiceHelper; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; import java.util.UUID; @@ -31,7 +31,7 @@ public class LoginMemberArgumentResolverTest { private JwtService jwtService; @Mock - private MemberServiceHelper memberServiceHelper; + private MemberRepository memberRepository; @Mock private NativeWebRequest webRequest; @@ -69,7 +69,7 @@ public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Excep when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); - when(memberServiceHelper.findByMemberId(memberId)).thenReturn(member); + when(memberRepository.findById(memberId)).thenReturn(member); // then Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); From b9767b33c8467a8f08e367574341b97df2b67cdb Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:19:17 +0900 Subject: [PATCH 132/478] =?UTF-8?q?feat:=20(#55)=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20id=EB=A5=BC=20Long=EC=9C=BC=EB=A1=9C=20autoIncremen?= =?UTF-8?q?t=ED=95=98=EB=8A=94=20BaseEntity=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/shared/BaseLongIdEntity.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseLongIdEntity.java diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseLongIdEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseLongIdEntity.java new file mode 100644 index 000000000..1645d0e26 --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseLongIdEntity.java @@ -0,0 +1,34 @@ +package spring.backend.core.infrastructure.jpa.shared; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(value = AuditingEntityListener.class) +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BaseLongIdEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long id; + + @CreatedDate + protected LocalDateTime createdAt; + + @LastModifiedDate + protected LocalDateTime updatedAt; + + @Builder.Default + protected Boolean deleted = false; +} \ No newline at end of file From 72d667449449079eaba112752f9d4f6768be0493 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:21:11 +0900 Subject: [PATCH 133/478] =?UTF-8?q?feat:=20(#55)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=98=20ErrorCode=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/exception/ActivityErrorCode.java | 23 +++++++++++++++++++ .../exception/QuickStartErrorCode.java | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/java/spring/backend/activity/exception/ActivityErrorCode.java create mode 100644 src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java diff --git a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java new file mode 100644 index 000000000..272b76fe5 --- /dev/null +++ b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java @@ -0,0 +1,23 @@ +package spring.backend.activity.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum ActivityErrorCode implements BaseErrorCode { + + ACTIVITY_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "활동을 저장하는데 실패하였습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java new file mode 100644 index 000000000..e9a54f255 --- /dev/null +++ b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java @@ -0,0 +1,23 @@ +package spring.backend.activity.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum QuickStartErrorCode implements BaseErrorCode { + + QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From 43b1988ee5209ab0d48588e2da10e8d3a7685e84 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:22:59 +0900 Subject: [PATCH 134/478] =?UTF-8?q?feat:=20(#55)=20Activity=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20?= =?UTF-8?q?=EB=B0=B8=EB=A5=98=ED=83=80=EC=9E=85,=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=84=B0=EB=A6=AC=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/entity/Activity.java | 45 +++++++++++++++++++ .../domain/repository/ActivityRepository.java | 9 ++++ .../activity/domain/value/Keyword.java | 37 +++++++++++++++ .../backend/activity/domain/value/Type.java | 14 ++++++ 4 files changed, 105 insertions(+) create mode 100644 src/main/java/spring/backend/activity/domain/entity/Activity.java create mode 100644 src/main/java/spring/backend/activity/domain/repository/ActivityRepository.java create mode 100644 src/main/java/spring/backend/activity/domain/value/Keyword.java create mode 100644 src/main/java/spring/backend/activity/domain/value/Type.java diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java new file mode 100644 index 000000000..76ff86b7a --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -0,0 +1,45 @@ +package spring.backend.activity.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; + +import java.time.LocalDateTime; +import java.util.Set; +import java.util.UUID; + +@Getter +@Builder +public class Activity { + + private Long id; + + private UUID memberId; + + private Long quickStartId; + + private Integer spareTime; + + private Type type; + + private Set keywords; + + private String title; + + private String content; + + private String location; + + private Boolean finished; + + private LocalDateTime finishedAt; + + private Integer savedTime; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + private Boolean deleted; +} diff --git a/src/main/java/spring/backend/activity/domain/repository/ActivityRepository.java b/src/main/java/spring/backend/activity/domain/repository/ActivityRepository.java new file mode 100644 index 000000000..3226141d9 --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/repository/ActivityRepository.java @@ -0,0 +1,9 @@ +package spring.backend.activity.domain.repository; + +import spring.backend.activity.domain.entity.Activity; + +public interface ActivityRepository { + + Activity findById(Long id); + Activity save(Activity Activity); +} diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java new file mode 100644 index 000000000..dc7e450a2 --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -0,0 +1,37 @@ +package spring.backend.activity.domain.value; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.*; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class Keyword { + + @Enumerated(EnumType.STRING) + private Category category; + + private String image; + + @Getter + @RequiredArgsConstructor + public enum Category { + SELF_DEVELOPMENT("자기개발"), + HEALTH("건강"), + NATURE("자연"), + CULTURE_ART("문화/예술"), + ENTERTAINMENT("엔터테인먼트"), + RELAXATION("휴식"), + SOCIAL("소셜"); + + private final String description; + } + + public static Keyword create(Category category, String image) { + return new Keyword(category, image); + } +} diff --git a/src/main/java/spring/backend/activity/domain/value/Type.java b/src/main/java/spring/backend/activity/domain/value/Type.java new file mode 100644 index 000000000..e872ed3cb --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/value/Type.java @@ -0,0 +1,14 @@ +package spring.backend.activity.domain.value; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Type { + ONLINE("온라인"), + OFFLINE("오프라인"), + ONLINE_AND_OFFLINE("둘 다"); + + private final String description; +} From 00e62066b0beca0ac71079544f43d919d881e500 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:23:41 +0900 Subject: [PATCH 135/478] =?UTF-8?q?feat:=20(#55)=20QuickStart=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/entity/QuickStart.java | 32 +++++++++++++++++++ .../repository/QuickStartRepository.java | 9 ++++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/spring/backend/activity/domain/entity/QuickStart.java create mode 100644 src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java diff --git a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java new file mode 100644 index 000000000..64c368c93 --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java @@ -0,0 +1,32 @@ +package spring.backend.activity.domain.entity; + +import lombok.Builder; +import lombok.Getter; +import spring.backend.activity.domain.value.Type; + +import java.sql.Time; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Builder +public class QuickStart { + + private Long id; + + private UUID memberId; + + private String name; + + private Time startTime; + + private Integer spareTime; + + private Type type; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + private Boolean deleted; +} diff --git a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java new file mode 100644 index 000000000..5e483a366 --- /dev/null +++ b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java @@ -0,0 +1,9 @@ +package spring.backend.activity.domain.repository; + +import spring.backend.activity.domain.entity.QuickStart; + +public interface QuickStartRepository { + + QuickStart findById(Long id); + QuickStart save(QuickStart quickStart); +} From d4a2601c95833495ef11127be31ac1bc179e24da Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:25:10 +0900 Subject: [PATCH 136/478] =?UTF-8?q?feat:=20(#55)=20Activity=20Jpa=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=84=B0=EB=A6=AC=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/entity/ActivityJpaEntity.java | 48 +++++++++++++++++++ .../jpa/repository/ActivityJpaRepository.java | 7 +++ 2 files changed, 55 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/ActivityJpaRepository.java diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java new file mode 100644 index 000000000..7065b4d78 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java @@ -0,0 +1,48 @@ +package spring.backend.activity.infrastructure.persistence.jpa.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; +import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; + +import java.time.LocalDateTime; +import java.util.Set; +import java.util.UUID; + +@Entity +@Table(name = "activity") +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ActivityJpaEntity extends BaseLongIdEntity { + + private UUID memberId; + + private Long quickStartId; + + private Integer spareTime; + + @Enumerated(EnumType.STRING) + private Type type; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "activity_keyword", + joinColumns = @JoinColumn(name = "activity_id")) + private Set keywords; + + private String title; + + private String content; + + private String location; + + private Boolean finished; + + private LocalDateTime finishedAt; + + private Integer savedTime; +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/ActivityJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/ActivityJpaRepository.java new file mode 100644 index 000000000..e882aab1d --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/ActivityJpaRepository.java @@ -0,0 +1,7 @@ +package spring.backend.activity.infrastructure.persistence.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; + +public interface ActivityJpaRepository extends JpaRepository { +} From bdd3112dfd4e7b11532bf3a68bf06fb09d45185d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:25:33 +0900 Subject: [PATCH 137/478] =?UTF-8?q?feat:=20(#55)=20QuickStart=20Jpa=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=84=B0=EB=A6=AC=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/entity/QuickStartJpaEntity.java | 34 +++++++++++++++++++ .../repository/QuickStartJpaRepository.java | 7 ++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java new file mode 100644 index 000000000..eef5db39c --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java @@ -0,0 +1,34 @@ +package spring.backend.activity.infrastructure.persistence.jpa.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import spring.backend.activity.domain.value.Type; +import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; + +import java.sql.Time; +import java.util.UUID; + +@Entity +@Table(name = "quick_start") +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class QuickStartJpaEntity extends BaseLongIdEntity { + + private UUID memberId; + + private String name; + + private Time startTime; + + private Integer spareTime; + + @Enumerated(EnumType.STRING) + private Type type; +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java new file mode 100644 index 000000000..548dafd6c --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java @@ -0,0 +1,7 @@ +package spring.backend.activity.infrastructure.persistence.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; + +public interface QuickStartJpaRepository extends JpaRepository { +} From d11ae6363dbbf437ac8baf0a34014d46b913230d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:26:35 +0900 Subject: [PATCH 138/478] =?UTF-8?q?feat:=20(#55)=20Activity=20=EB=A7=A4?= =?UTF-8?q?=ED=8D=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=99=80=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=20=EC=96=B4=EB=8C=91?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/mapper/ActivityMapper.java | 51 +++++++++++++++++++ .../jpa/adapter/ActivityRepositoryImpl.java | 41 +++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/ActivityRepositoryImpl.java diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java new file mode 100644 index 000000000..9581e4354 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java @@ -0,0 +1,51 @@ +package spring.backend.activity.infrastructure.mapper; + +import org.springframework.stereotype.Component; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; + +import java.util.Optional; + +@Component +public class ActivityMapper { + + public Activity toDomainEntity(ActivityJpaEntity activity) { + return Activity.builder() + .id(activity.getId()) + .memberId(activity.getMemberId()) + .quickStartId(activity.getQuickStartId()) + .spareTime(activity.getSpareTime()) + .type(activity.getType()) + .keywords(activity.getKeywords()) + .title(activity.getTitle()) + .content(activity.getContent()) + .location(activity.getLocation()) + .finished(activity.getFinished()) + .finishedAt(activity.getFinishedAt()) + .savedTime(activity.getSavedTime()) + .createdAt(activity.getCreatedAt()) + .updatedAt(activity.getUpdatedAt()) + .deleted(activity.getDeleted()) + .build(); + } + + public ActivityJpaEntity toJpaEntity(Activity activity) { + return ActivityJpaEntity.builder() + .id(activity.getId()) + .memberId(activity.getMemberId()) + .quickStartId(activity.getQuickStartId()) + .spareTime(activity.getSpareTime()) + .type(activity.getType()) + .keywords(activity.getKeywords()) + .title(activity.getTitle()) + .content(activity.getContent()) + .location(activity.getLocation()) + .finished(activity.getFinished()) + .finishedAt(activity.getFinishedAt()) + .savedTime(activity.getSavedTime()) + .createdAt(activity.getCreatedAt()) + .updatedAt(activity.getUpdatedAt()) + .deleted(Optional.ofNullable(activity.getDeleted()).orElse(false)) + .build(); + } +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/ActivityRepositoryImpl.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/ActivityRepositoryImpl.java new file mode 100644 index 000000000..884b64298 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/ActivityRepositoryImpl.java @@ -0,0 +1,41 @@ +package spring.backend.activity.infrastructure.persistence.jpa.adapter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Repository; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.activity.infrastructure.mapper.ActivityMapper; +import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.repository.ActivityJpaRepository; + +@Repository +@RequiredArgsConstructor +@Log4j2 +public class ActivityRepositoryImpl implements ActivityRepository { + + private final ActivityMapper activityMapper; + private final ActivityJpaRepository activityJpaRepository; + + @Override + public Activity findById(Long id) { + ActivityJpaEntity activityJpaEntity = activityJpaRepository.findById(id).orElse(null); + if (activityJpaEntity == null) { + return null; + } + return activityMapper.toDomainEntity(activityJpaEntity); + } + + @Override + public Activity save(Activity activity) { + try { + ActivityJpaEntity activityJpaEntity = activityMapper.toJpaEntity(activity); + activityJpaRepository.save(activityJpaEntity); + return activityMapper.toDomainEntity(activityJpaEntity); + } catch (Exception e) { + log.error("[ActivityRepositoryImpl] Failed to save activity", e); + throw ActivityErrorCode.ACTIVITY_SAVE_FAILED.toException(); + } + } +} From a6c62a631cba0efec085f1be362ba2ac9ea17dee Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 06:26:46 +0900 Subject: [PATCH 139/478] =?UTF-8?q?feat:=20(#55)=20QuickStart=20=EB=A7=A4?= =?UTF-8?q?=ED=8D=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=99=80=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=20=EC=96=B4=EB=8C=91?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/QuickStartMapper.java | 39 ++++++++++++++++++ .../jpa/adapter/QuickStartRepositoryImpl.java | 41 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java new file mode 100644 index 000000000..dbece55fe --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java @@ -0,0 +1,39 @@ +package spring.backend.activity.infrastructure.mapper; + +import org.springframework.stereotype.Component; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; + +import java.util.Optional; + +@Component +public class QuickStartMapper { + + public QuickStart toDomainEntity(QuickStartJpaEntity quickStart) { + return QuickStart.builder() + .id(quickStart.getId()) + .memberId(quickStart.getMemberId()) + .name(quickStart.getName()) + .startTime(quickStart.getStartTime()) + .spareTime(quickStart.getSpareTime()) + .type(quickStart.getType()) + .createdAt(quickStart.getCreatedAt()) + .updatedAt(quickStart.getUpdatedAt()) + .deleted(quickStart.getDeleted()) + .build(); + } + + public QuickStartJpaEntity toJpaEntity(QuickStart quickStart) { + return QuickStartJpaEntity.builder() + .id(quickStart.getId()) + .memberId(quickStart.getMemberId()) + .name(quickStart.getName()) + .startTime(quickStart.getStartTime()) + .spareTime(quickStart.getSpareTime()) + .type(quickStart.getType()) + .createdAt(quickStart.getCreatedAt()) + .updatedAt(quickStart.getUpdatedAt()) + .deleted(Optional.ofNullable(quickStart.getDeleted()).orElse(false)) + .build(); + } +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java new file mode 100644 index 000000000..1545e6f62 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java @@ -0,0 +1,41 @@ +package spring.backend.activity.infrastructure.persistence.jpa.adapter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Repository; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.activity.infrastructure.mapper.QuickStartMapper; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; + +@Repository +@RequiredArgsConstructor +@Log4j2 +public class QuickStartRepositoryImpl implements QuickStartRepository { + + private final QuickStartMapper quickStartMapper; + private final QuickStartJpaRepository quickStartJpaRepository; + + @Override + public QuickStart findById(Long id) { + QuickStartJpaEntity quickStartJpaEntity = quickStartJpaRepository.findById(id).orElse(null); + if (quickStartJpaEntity == null) { + return null; + } + return quickStartMapper.toDomainEntity(quickStartJpaEntity); + } + + @Override + public QuickStart save(QuickStart quickStart) { + try { + QuickStartJpaEntity quickStartJpaEntity = quickStartMapper.toJpaEntity(quickStart); + quickStartJpaRepository.save(quickStartJpaEntity); + return quickStartMapper.toDomainEntity(quickStartJpaEntity); + } catch (Exception e) { + log.error("[QuickStartRepositoryImpl] Failed to save quickStart", e); + throw QuickStartErrorCode.QUICK_START_SAVE_FAILED.toException(); + } + } +} From 5fc29cc5fb811203d246b794ff993d76653c2a82 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 28 Oct 2024 07:05:26 +0900 Subject: [PATCH 140/478] =?UTF-8?q?feat:=20(#55)=20Activity,=20QuickStart?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ActivityRepositoryTest.java | 53 +++++++++++++++++++ .../repository/QuickStartRepositoryTest.java | 46 ++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java create mode 100644 src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java diff --git a/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java new file mode 100644 index 000000000..be8b5ae02 --- /dev/null +++ b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java @@ -0,0 +1,53 @@ +package spring.backend.activity.domain.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; + +import java.time.LocalDateTime; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class ActivityRepositoryTest { + + @Autowired + private ActivityRepository activityRepository; + + private Activity activity; + + @BeforeEach + void setUp() { + activity = Activity.builder() + .memberId(UUID.randomUUID()) + .quickStartId(100L) + .spareTime(120) + .type(Type.ONLINE) + .keywords(Set.of(Keyword.create(Keyword.Category.SELF_DEVELOPMENT, "test.url"), Keyword.create(Keyword.Category.ENTERTAINMENT, "test1.url"))) + .title("Test Activity") + .content("This is a test activity.") + .location("Test Location") + .finished(false) + .finishedAt(null) + .savedTime(30) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .deleted(false) + .build(); + } + + @Test + void testSaveAndFindActivity() { + Activity savedActivity = activityRepository.save(activity); + Activity foundActivity = activityRepository.findById(savedActivity.getId()); + + assertThat(foundActivity).isNotNull(); + assertThat(foundActivity.getKeywords()).isEqualTo(activity.getKeywords()); + } +} \ No newline at end of file diff --git a/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java b/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java new file mode 100644 index 000000000..87b0c87ac --- /dev/null +++ b/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java @@ -0,0 +1,46 @@ +package spring.backend.activity.domain.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.value.Type; + +import java.sql.Time; +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class QuickStartRepositoryTest { + + @Autowired + private QuickStartRepository quickStartRepository; + + private QuickStart quickStart; + + @BeforeEach + void setUp() { + quickStart = QuickStart.builder() + .memberId(UUID.randomUUID()) + .name("Test QuickStart") + .startTime(Time.valueOf("01:02:03")) + .spareTime(60) + .type(Type.ONLINE) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .deleted(false) + .build(); + } + + @Test + void testSaveAndFindQuickStart() { + QuickStart savedQuickStart = quickStartRepository.save(quickStart); + QuickStart foundQuickStart = quickStartRepository.findById(savedQuickStart.getId()); + + assertThat(foundQuickStart).isNotNull(); + assertThat(foundQuickStart.getStartTime()).isEqualTo(quickStart.getStartTime()); + } +} \ No newline at end of file From d81471618c9b0b235c431a53a5d3b2c91d882821 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 29 Oct 2024 23:17:21 +0900 Subject: [PATCH 141/478] =?UTF-8?q?feat:=20(#58)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CreateQuickStartService.java | 42 +++++++++++++++++++ .../activity/domain/entity/QuickStart.java | 10 +++++ .../dto/request/QuickStartRequest.java | 28 +++++++++++++ .../exception/QuickStartErrorCode.java | 4 +- .../CreateQuickStartController.java | 29 +++++++++++++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/application/CreateQuickStartService.java create mode 100644 src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java create mode 100644 src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java diff --git a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java new file mode 100644 index 000000000..c6446c041 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java @@ -0,0 +1,42 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.member.domain.entity.Member; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional +public class CreateQuickStartService { + + private final QuickStartRepository quickStartRepository; + + public Long createQuickStart(Member member, QuickStartRequest request) { + validateRequest(request); + validateMember(member); + QuickStart quickStart = QuickStart.create(member.getId(), request.name(), request.startTime(), request.spareTime(), request.type()); + QuickStart savedQuickStart = quickStartRepository.save(quickStart); + return savedQuickStart.getId(); + } + + private void validateRequest(QuickStartRequest request) { + if (request == null) { + log.error("[CreateQuickStartService] Invalid request."); + throw QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.toException(); + } + } + + private void validateMember(Member member) { + if (!member.isMember()) { + log.error("[CreateQuickStartService] Client is not a member."); + throw QuickStartErrorCode.NOT_A_MEMBER.toException(); + } + } +} diff --git a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java index 64c368c93..374a1ec91 100644 --- a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java +++ b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java @@ -29,4 +29,14 @@ public class QuickStart { private LocalDateTime updatedAt; private Boolean deleted; + + public static QuickStart create(UUID memberId, String name, Time startTime, Integer spareTime, Type type) { + return QuickStart.builder() + .memberId(memberId) + .name(name) + .startTime(startTime) + .spareTime(spareTime) + .type(type) + .build(); + } } diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java new file mode 100644 index 000000000..752a05c75 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java @@ -0,0 +1,28 @@ +package spring.backend.activity.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import spring.backend.activity.domain.value.Type; + +import java.sql.Time; + +public record QuickStartRequest( + + @NotNull(message = "이름은 필수 입력 항목입니다.") + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,10}$", message = "이름은 한글, 영문, 숫자만 입력 가능하며 최대 10자까지 입력 가능합니다.") + String name, + + @NotNull(message = "시작 시간은 필수 입력 항목입니다.") + Time startTime, + + @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") + @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") + @Max(value = 300, message = "자투리 시간은 최대 300이어야 합니다.") + Integer spareTime, + + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + Type type +) { +} diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java index e9a54f255..d50756225 100644 --- a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java @@ -10,7 +10,9 @@ @RequiredArgsConstructor public enum QuickStartErrorCode implements BaseErrorCode { - QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."); + QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."), + NOT_EXIST_QUICK_START_CONDITION(HttpStatus.BAD_REQUEST, "빠른 시작 요청 조건이 유효하지 않습니다."), + NOT_A_MEMBER(HttpStatus.FORBIDDEN, "사용자가 멤버 사용자가 아닙니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java b/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java new file mode 100644 index 000000000..6313b5d5d --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java @@ -0,0 +1,29 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.CreateQuickStartService; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.presentation.swagger.CreateQuickStartSwagger; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class CreateQuickStartController implements CreateQuickStartSwagger { + + private final CreateQuickStartService createQuickStartService; + + @Authorization + @PostMapping("/v1/quick-starts") + public ResponseEntity> createQuickStart(@LoginMember Member member, @Valid @RequestBody QuickStartRequest request) { + Long memberId = createQuickStartService.createQuickStart(member, request); + return ResponseEntity.ok(new RestResponse<>(memberId)); + } +} From 048929544b9ee40cf9533ab11f3d6ad4f4b7a9c4 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 29 Oct 2024 23:22:48 +0900 Subject: [PATCH 142/478] =?UTF-8?q?feat:=20(#58)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=83=9D=EC=84=B1=20API=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/QuickStartRequest.java | 5 ++++ .../swagger/CreateQuickStartSwagger.java | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/CreateQuickStartSwagger.java diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java index 752a05c75..f851b3ffa 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java @@ -1,5 +1,6 @@ package spring.backend.activity.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; @@ -12,17 +13,21 @@ public record QuickStartRequest( @NotNull(message = "이름은 필수 입력 항목입니다.") @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,10}$", message = "이름은 한글, 영문, 숫자만 입력 가능하며 최대 10자까지 입력 가능합니다.") + @Schema(description = "빠른 시작 이름", example = "등교") String name, @NotNull(message = "시작 시간은 필수 입력 항목입니다.") + @Schema(description = "시작 시간", example = "12:30:00") Time startTime, @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") @Max(value = 300, message = "자투리 시간은 최대 300이어야 합니다.") + @Schema(description = "자투리 시간", example = "300") Integer spareTime, @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") Type type ) { } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/CreateQuickStartSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/CreateQuickStartSwagger.java new file mode 100644 index 000000000..f43aabff8 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/CreateQuickStartSwagger.java @@ -0,0 +1,24 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "QuickStart", description = "빠른 시작") +public interface CreateQuickStartSwagger { + + @Operation( + summary = "빠른 시작 생성 API", + description = "빠른 시작을 생성합니다.", + operationId = "/v1/quick-starts" + ) + @ApiErrorCode({GlobalErrorCode.class, QuickStartErrorCode.class}) + ResponseEntity> createQuickStart(@Parameter(hidden = true) Member member, QuickStartRequest request); +} From 34539c1d2d7af00d3a293b56ae214df42d37a30c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 29 Oct 2024 23:42:37 +0900 Subject: [PATCH 143/478] =?UTF-8?q?feat:=20(#58)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=83=9D=EC=84=B1=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateQuickStartServiceTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java diff --git a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java new file mode 100644 index 000000000..a711c1538 --- /dev/null +++ b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java @@ -0,0 +1,88 @@ +package spring.backend.activity.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.activity.domain.value.Type; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Role; + +import java.sql.Time; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CreateQuickStartServiceTest { + + @InjectMocks + private CreateQuickStartService createQuickStartService; + + @Mock + private QuickStartRepository quickStartRepository; + + private Member member; + private Member guest; + private QuickStartRequest request; + + @BeforeEach + public void setUp() { + member = Member.builder() + .role(Role.MEMBER) + .build(); + guest = Member.builder() + .role(Role.GUEST) + .build(); + request = new QuickStartRequest( + "등교", + Time.valueOf("12:30:00"), + 300, + Type.ONLINE + ); + } + + @DisplayName("요청이 null인 경우 예외가 발생한다") + @Test + public void createQuickStart_NullRequest_ThrowsException() { + // when + DomainException ex = assertThrows(DomainException.class, () -> createQuickStartService.createQuickStart(member, null)); + + // then + assertEquals(QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.getMessage(), ex.getMessage()); + } + + @DisplayName("비회원이 요청하는 경우 예외가 발생한다") + @Test + public void createQuickStart_NonMember_ThrowsException() { + // when + DomainException ex = assertThrows(DomainException.class, () -> createQuickStartService.createQuickStart(guest, request)); + + // then + assertEquals(QuickStartErrorCode.NOT_A_MEMBER.getMessage(), ex.getMessage()); + } + + @DisplayName("유효한 빠른 시작 요청인 경우 저장된 ID를 반환한다") + @Test + public void createQuickStart_ValidRequest_ReturnsSavedQuickStartId() { + QuickStart quickStart = QuickStart.create(member.getId(), request.name(), request.startTime(), request.spareTime(), request.type()); + when(quickStartRepository.save(any(QuickStart.class))).thenReturn(quickStart); + + // when + Long savedQuickStartId = createQuickStartService.createQuickStart(member, request); + + // then + assertEquals(quickStart.getId(), savedQuickStartId); + verify(quickStartRepository).save(any(QuickStart.class)); + } +} \ No newline at end of file From 9cf3c57085e37afcdcbbe62302ae8e4f884d62b5 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 16:14:24 +0900 Subject: [PATCH 144/478] =?UTF-8?q?fix:=20(#58)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=9D=B4=EB=A6=84=EC=9D=98=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=A1=B0=EA=B1=B4=EC=9D=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/dto/request/QuickStartRequest.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java index f851b3ffa..5fc56c165 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java @@ -1,10 +1,7 @@ package spring.backend.activity.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.*; import spring.backend.activity.domain.value.Type; import java.sql.Time; @@ -12,7 +9,8 @@ public record QuickStartRequest( @NotNull(message = "이름은 필수 입력 항목입니다.") - @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,10}$", message = "이름은 한글, 영문, 숫자만 입력 가능하며 최대 10자까지 입력 가능합니다.") + @Pattern(regexp = "^(?!\\s)([a-zA-Z0-9가-힣]+(\\s[a-zA-Z0-9가-힣]+)*)?$", message = "이름은 한글, 영문, 숫자 및 공백만 입력 가능하며, 공백으로 시작하거나 끝날 수 없고, 연속된 공백이 없어야 합니다.") + @Size(max = 10, message = "최대 10자까지 입력 가능합니다.") @Schema(description = "빠른 시작 이름", example = "등교") String name, From d42a8960b3a8540709fa34731fcada5fafee04c0 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 31 Oct 2024 03:26:46 +0900 Subject: [PATCH 145/478] =?UTF-8?q?feat:=20(#58)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=9D=B4=EB=A6=84=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/QuickStartRequestTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java diff --git a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java new file mode 100644 index 000000000..33e8a36e6 --- /dev/null +++ b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java @@ -0,0 +1,75 @@ +package spring.backend.activity.dto.request; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import spring.backend.activity.domain.value.Type; + +import java.sql.Time; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuickStartRequestTest { + + private final Validator validator; + + public QuickStartRequestTest() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + this.validator = factory.getValidator(); + } + } + + @Nested + @DisplayName("name 필드 검증 테스트") + class NameValidationTests { + + @Test + @DisplayName("null 값일 경우 에러가 발생한다.") + void whenNameIsNull_thenValidationFails() { + QuickStartRequest request = new QuickStartRequest(null, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + Set> violations = validator.validate(request); + + assertThat(violations).isNotEmpty(); + assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 필수 입력 항목입니다.")); + } + + @ParameterizedTest + @DisplayName("올바른 형식의 이름일 경우 성공한다.") + @ValueSource(strings = {"등교", "이름테스트", "John Doe", "사용자1"}) + void whenNameIsValid_thenValidationSucceeds(String name) { + QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + Set> violations = validator.validate(request); + + assertThat(violations).isEmpty(); + } + + @ParameterizedTest + @DisplayName("형식에 맞지 않는 이름일 경우 에러가 발생한다.") + @ValueSource(strings = {" 이름", "이름 ", "이름@이름", "공백 공백"}) + void whenNameIsInvalid_thenValidationFails(String name) { + QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + Set> violations = validator.validate(request); + + assertThat(violations).isNotEmpty(); + assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 한글, 영문, 숫자 및 공백만 입력 가능하며")); + } + + @Test + @DisplayName("10자를 초과하는 경우 에러가 발생한다.") + void whenNameExceedsMaxLength_thenValidationFails() { + String name = "매우몹시너무긴이름longname"; + QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + Set> violations = validator.validate(request); + + assertThat(violations).isNotEmpty(); + assertThat(violations).anyMatch(violation -> violation.getMessage().contains("최대 10자까지 입력 가능합니다.")); + } + } +} \ No newline at end of file From a8ab287adede44b22a15172c07ffc772c1690b38 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 02:49:44 +0900 Subject: [PATCH 146/478] =?UTF-8?q?feat:=20(#60)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=A1=B0=ED=9A=8C=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?DAO=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/QuickStartJpaDao.java | 28 +++++++++++++++++++ .../activity/query/dao/QuickStartDao.java | 12 ++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java create mode 100644 src/main/java/spring/backend/activity/query/dao/QuickStartDao.java diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java new file mode 100644 index 000000000..834793047 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java @@ -0,0 +1,28 @@ +package spring.backend.activity.infrastructure.persistence.jpa.dao; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import spring.backend.activity.dto.response.QuickStartResponse; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.activity.query.dao.QuickStartDao; + +import java.util.List; +import java.util.UUID; + +public interface QuickStartJpaDao extends JpaRepository, QuickStartDao { + + @Override + @Query(""" + select new spring.backend.activity.dto.response.QuickStartResponse( + q.id, + q.name, + q.startTime, + q.spareTime, + q.type + ) + from QuickStartJpaEntity q + where q.memberId = :memberId + """) + List findByMemberId(UUID memberId, Sort sort); +} \ No newline at end of file diff --git a/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java b/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java new file mode 100644 index 000000000..4183651c8 --- /dev/null +++ b/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java @@ -0,0 +1,12 @@ +package spring.backend.activity.query.dao; + +import org.springframework.data.domain.Sort; +import spring.backend.activity.dto.response.QuickStartResponse; + +import java.util.List; +import java.util.UUID; + +public interface QuickStartDao { + + List findByMemberId(UUID memberId, Sort sort); +} From 24e1c31c090fd1ba37ceae7ce688ff57d5aee23d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 02:50:25 +0900 Subject: [PATCH 147/478] =?UTF-8?q?feat:=20(#60)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadQuickStartsService.java | 36 +++++++++++++++++++ .../dto/response/QuickStartResponse.java | 14 ++++++++ .../dto/response/QuickStartsResponse.java | 8 +++++ .../ReadQuickStartsController.java | 25 +++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/ReadQuickStartsService.java create mode 100644 src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java create mode 100644 src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java create mode 100644 src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java diff --git a/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java b/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java new file mode 100644 index 000000000..d06d51789 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java @@ -0,0 +1,36 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.dto.response.QuickStartResponse; +import spring.backend.activity.dto.response.QuickStartsResponse; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.activity.query.dao.QuickStartDao; +import spring.backend.member.domain.entity.Member; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional(readOnly = true) +public class ReadQuickStartsService { + + private final QuickStartDao quickStartDao; + + public QuickStartsResponse readQuickStarts(Member member) { + validateMember(member); + List quickStartResponses = quickStartDao.findByMemberId(member.getId(), Sort.by("createdAt").descending()); + return new QuickStartsResponse(quickStartResponses); + } + + private void validateMember(Member member) { + if (!member.isMember()) { + log.error("[ReadQuickStartsService] Client is not a member."); + throw QuickStartErrorCode.NOT_A_MEMBER.toException(); + } + } +} diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java new file mode 100644 index 000000000..58c841413 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java @@ -0,0 +1,14 @@ +package spring.backend.activity.dto.response; + +import spring.backend.activity.domain.value.Type; + +import java.sql.Time; + +public record QuickStartResponse( + Long id, + String name, + Time startTime, + Integer spareTime, + Type type +) { +} diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java new file mode 100644 index 000000000..e1e256cef --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java @@ -0,0 +1,8 @@ +package spring.backend.activity.dto.response; + +import java.util.List; + +public record QuickStartsResponse( + List quickStartResponses +) { +} diff --git a/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java new file mode 100644 index 000000000..1ff8b10f9 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java @@ -0,0 +1,25 @@ +package spring.backend.activity.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.ReadQuickStartsService; +import spring.backend.activity.dto.response.QuickStartsResponse; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class ReadQuickStartsController { + + private final ReadQuickStartsService readQuickStartsService; + + @Authorization + @GetMapping("/v1/quick-starts") + public ResponseEntity> readQuickStarts(@LoginMember Member member) { + return ResponseEntity.ok(new RestResponse<>(readQuickStartsService.readQuickStarts(member))); + } +} From 9f3564bfa7bdf448388bcac2930fbbdf5a33f23e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 03:02:23 +0900 Subject: [PATCH 148/478] =?UTF-8?q?feat:=20(#60)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/QuickStartResponse.java | 11 +++++++++ .../dto/response/QuickStartsResponse.java | 4 ++++ .../ReadQuickStartsController.java | 3 ++- .../swagger/ReadQuickStartsSwagger.java | 24 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/ReadQuickStartsSwagger.java diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java index 58c841413..f86945b4d 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java @@ -1,14 +1,25 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Type; import java.sql.Time; public record QuickStartResponse( + + @Schema(description = "빠른 시작 ID", example = "1") Long id, + + @Schema(description = "빠른 시작 이름", example = "등교") String name, + + @Schema(description = "시작 시간", example = "12:30:00") Time startTime, + + @Schema(description = "자투리 시간", example = "300") Integer spareTime, + + @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") Type type ) { } diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java index e1e256cef..55d3c9928 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java @@ -1,8 +1,12 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; public record QuickStartsResponse( + + @Schema(description = "빠른 시작 리스트") List quickStartResponses ) { } diff --git a/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java index 1ff8b10f9..9c1d1ea21 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadQuickStartsService; import spring.backend.activity.dto.response.QuickStartsResponse; +import spring.backend.activity.presentation.swagger.ReadQuickStartsSwagger; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; @@ -13,7 +14,7 @@ @RestController @RequiredArgsConstructor -public class ReadQuickStartsController { +public class ReadQuickStartsController implements ReadQuickStartsSwagger { private final ReadQuickStartsService readQuickStartsService; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadQuickStartsSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadQuickStartsSwagger.java new file mode 100644 index 000000000..64811fb08 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadQuickStartsSwagger.java @@ -0,0 +1,24 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.response.QuickStartsResponse; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "QuickStart", description = "빠른 시작") +public interface ReadQuickStartsSwagger { + + @Operation( + summary = "빠른 시작 리스트 조회 API", + description = "사용자가 생성한 빠른 시작 리스트를 반환합니다.", + operationId = "/v1/quick-starts" + ) + @ApiErrorCode({GlobalErrorCode.class, QuickStartErrorCode.class}) + ResponseEntity> readQuickStarts(@Parameter(hidden = true) Member member); +} From b7705abbc37f8ea1dae8f54f4d181c66c1185d1a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 03:51:41 +0900 Subject: [PATCH 149/478] =?UTF-8?q?feat:=20(#62)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/UpdateQuickStartService.java | 64 +++++++++++++++++++ .../activity/domain/entity/QuickStart.java | 7 ++ .../exception/QuickStartErrorCode.java | 4 +- .../UpdateQuickStartController.java | 28 ++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/application/UpdateQuickStartService.java create mode 100644 src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java diff --git a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java new file mode 100644 index 000000000..c260eb82f --- /dev/null +++ b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java @@ -0,0 +1,64 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional +public class UpdateQuickStartService { + + private final QuickStartRepository quickStartRepository; + + public void updateQuickStart(Member member, QuickStartRequest request, Long quickStartId) { + QuickStart quickStart = quickStartRepository.findById(quickStartId); + validateUpdateRequest(member, request, quickStart); + quickStart.update(request.name(), request.startTime(), request.spareTime(), request.type()); + quickStartRepository.save(quickStart); + } + + private void validateUpdateRequest(Member member, QuickStartRequest request, QuickStart quickStart) { + validateQuickStartExistence(quickStart); + validateRequest(request); + validateMember(member); + validateMemberId(member.getId(), quickStart.getMemberId()); + } + + private void validateQuickStartExistence(QuickStart quickStart) { + if (quickStart == null) { + log.error("[validateQuickStartExistence] QuickStart does not exist."); + throw QuickStartErrorCode.NOT_EXIST_QUICK_START.toException(); + } + } + + private void validateRequest(QuickStartRequest request) { + if (request == null) { + log.error("[validateRequest] Request is null."); + throw QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.toException(); + } + } + + private void validateMember(Member member) { + if (!member.isMember()) { + log.error("[validateMember] Unauthorized member."); + throw QuickStartErrorCode.NOT_A_MEMBER.toException(); + } + } + + private void validateMemberId(UUID memberId, UUID quickStartMemberId) { + if (!memberId.equals(quickStartMemberId)) { + log.error("[validateMemberId] Member id mismatch"); + throw QuickStartErrorCode.MEMBER_ID_MISMATCH.toException(); + } + } +} diff --git a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java index 374a1ec91..6f9411b78 100644 --- a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java +++ b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java @@ -39,4 +39,11 @@ public static QuickStart create(UUID memberId, String name, Time startTime, Inte .type(type) .build(); } + + public void update(String name, Time startTime, Integer spareTime, Type type) { + this.name = name; + this.startTime = startTime; + this.spareTime = spareTime; + this.type = type; + } } diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java index d50756225..c91701918 100644 --- a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java @@ -12,7 +12,9 @@ public enum QuickStartErrorCode implements BaseErrorCode { QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."), NOT_EXIST_QUICK_START_CONDITION(HttpStatus.BAD_REQUEST, "빠른 시작 요청 조건이 유효하지 않습니다."), - NOT_A_MEMBER(HttpStatus.FORBIDDEN, "사용자가 멤버 사용자가 아닙니다."); + NOT_A_MEMBER(HttpStatus.FORBIDDEN, "사용자가 멤버 사용자가 아닙니다."), + NOT_EXIST_QUICK_START(HttpStatus.BAD_REQUEST, "빠른 시작이 존재하지 않습니다."), + MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "빠른 시작과 멤버 ID가 일치하지 않습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java new file mode 100644 index 000000000..a70e19787 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java @@ -0,0 +1,28 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.UpdateQuickStartService; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class UpdateQuickStartController { + + private final UpdateQuickStartService updateQuickStartService; + + @Authorization + @PatchMapping("/v1/quick-starts/{quickStartId}") + public ResponseEntity updateQuickStart(@LoginMember Member member, @Valid @RequestBody QuickStartRequest request, @PathVariable Long quickStartId) { + updateQuickStartService.updateQuickStart(member, request, quickStartId); + return ResponseEntity.ok().build(); + } +} From 8bb9171bfde59a8eec61fb39a1d1fd68a0beab47 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 30 Oct 2024 03:59:13 +0900 Subject: [PATCH 150/478] =?UTF-8?q?feat:=20(#62)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=88=98=EC=A0=95=20API=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UpdateQuickStartController.java | 3 ++- .../swagger/UpdateQuickStartSwagger.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/UpdateQuickStartSwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java index a70e19787..bc32ddd5f 100644 --- a/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java +++ b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java @@ -9,13 +9,14 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.UpdateQuickStartService; import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.presentation.swagger.UpdateQuickStartSwagger; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.member.domain.entity.Member; @RestController @RequiredArgsConstructor -public class UpdateQuickStartController { +public class UpdateQuickStartController implements UpdateQuickStartSwagger { private final UpdateQuickStartService updateQuickStartService; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UpdateQuickStartSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UpdateQuickStartSwagger.java new file mode 100644 index 000000000..d9ba14224 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/UpdateQuickStartSwagger.java @@ -0,0 +1,23 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.QuickStartRequest; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "QuickStart", description = "빠른 시작") +public interface UpdateQuickStartSwagger { + + @Operation( + summary = "빠른 시작 수정 API", + description = "빠른 시작 ID를 통해 빠른 시작을 수정합니다.", + operationId = "/v1/quick-starts/{quickStartId}" + ) + @ApiErrorCode({GlobalErrorCode.class, QuickStartErrorCode.class}) + ResponseEntity updateQuickStart(@Parameter(hidden = true) Member member, QuickStartRequest request, Long quickStartId); +} From 808c6e8a8f076d799b4052cf5118946b2416fe22 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:28:12 +0900 Subject: [PATCH 151/478] =?UTF-8?q?chore:=20(#50)=20webFlux=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EA=B3=BC=20Netty=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 071cc6866..8b48d4c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,13 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // WebFlux 외부 API 호출 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Netty + implementation "io.netty:netty-resolver-dns-native-macos:4.1.113.Final:osx-aarch_64" + } tasks.named('test') { From dce9b279c991ed13fd18bb7cdc8cd29acf86c232 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:28:36 +0900 Subject: [PATCH 152/478] =?UTF-8?q?feat:=20(#50)=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EC=9D=98=20=ED=99=9C=EB=8F=99=20=ED=83=80=EC=9E=85=20enum?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/domain/value/Type.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java new file mode 100644 index 000000000..ed40dc5a5 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java @@ -0,0 +1,10 @@ +package spring.backend.recommendation.infrastructure.clova.domain.value; + +import lombok.Getter; + +@Getter +public enum Type { + OFFLINE, + ONLINE, + OFFLINE_AND_ONLINE; +} \ No newline at end of file From bff3b1ff83906dc31fb197ceaefa936a708774a7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:28:58 +0900 Subject: [PATCH 153/478] =?UTF-8?q?chore:=20(#50)=20WebClient=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webclient/WebClientConfiguration.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java new file mode 100644 index 000000000..49be9ced1 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java @@ -0,0 +1,14 @@ +package spring.backend.core.configuration.webclient; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfiguration { + + @Bean + public WebClient webClient(WebClient.Builder builder) { + return builder.build(); + } +} From 7e61f22089b4fb4b85567584fbae41096c67c7d1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:30:11 +0900 Subject: [PATCH 154/478] =?UTF-8?q?feat:=20(#50)=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20=EB=B0=9B=EC=9D=84=20DTO=EB=A5=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/UserInputRequest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java diff --git a/src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java b/src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java new file mode 100644 index 000000000..9f19c0840 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java @@ -0,0 +1,27 @@ +package spring.backend.recommendation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import spring.backend.recommendation.infrastructure.clova.domain.value.Type; + +@Getter +public class UserInputRequest { + + @NotNull + @Pattern(regexp = "^(5|[1-9][0-9]|[1-2][0-9]{2}|300)$", message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") + @Schema(description = "자투리 시간", example = "30") + private int spareTime; + + @NotNull + @Schema(description = "활동 타입", example = "OFFLINE") + private Type activityType; + + @NotNull + @Schema(description = "활동 키워드", example = "문화/예술") + private String keyword; + + @Schema(description = "위치", example = "서울시 강남구") + private String location; +} \ No newline at end of file From d628323221d3a9a2fa4530c50e98312430387d73 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:47:47 +0900 Subject: [PATCH 155/478] =?UTF-8?q?feat:=20(#50)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=20=EC=8A=A4=ED=8A=9C=EB=94=94=EC=98=A4=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=B4=EB=82=BC=20Message=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/Message.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java new file mode 100644 index 000000000..c6082058f --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -0,0 +1,47 @@ +package spring.backend.recommendation.infrastructure.clova.dto.request; + + +import lombok.Builder; +import lombok.Getter; +import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; + +@Getter +@Builder +public class Message { + + private ROLE role; + private String content; + + public enum ROLE { + system, user + } + + public static Message createSystem() { + String content = "너는 자투리시간과 활동의 타입(온라인,오프라인, 둘다)를 받고, 키워드와 위치 정보를 받아 사용자에게 5가지의 활동을 추천해주는 봇이야. 추천목록만 보내주면 되고, 각 추천목록은 줄바꿈으로 구분해줘. 추천목록을 제외한 잡설은 보내지마."; + return Message.builder() + .role(ROLE.system) + .content(content) + .build(); + } + + public static Message createMessage(UserInputRequest userInputRequest) { + return Message.builder().role(ROLE.user).content(createContent(userInputRequest)).build(); + } + + private static String createContent(UserInputRequest userInputRequest) { + int spareTime = userInputRequest.getSpareTime(); + String activityType = userInputRequest.getActivityType().toString(); + String keyword = userInputRequest.getKeyword(); + String location = userInputRequest.getLocation(); + + if (activityType.equals("OFFLINE")) { + if (location == null) { + throw ClovaErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); + } + return "자투리 시간: " + spareTime + "분\\n선호활동: " + activityType + "\\n활동 키워드: " + keyword + "\\n위치 : " + location + "\\n \\n활동 추천해줘\\n\\n"; + } else { + return "자투리 시간: " + spareTime + "분\\n선호활동: " + activityType + "\\n활동 키워드: " + keyword + "\\n \\n활동 추천해줘\\n\\n"; + } + } +} \ No newline at end of file From b01b98d7261e0da4e54d0592b29c969e1b7bdc29 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:48:42 +0900 Subject: [PATCH 156/478] =?UTF-8?q?feat:=20(#50)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=20=EC=8A=A4=ED=8A=9C=EB=94=94=EC=98=A4=20=EC=9A=A9=20?= =?UTF-8?q?Request,=20Response=20DTO=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/ClovaRequest.java | 37 +++++++++++++++++++ .../clova/dto/response/ClovaResponse.java | 23 ++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/dto/response/ClovaResponse.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java new file mode 100644 index 000000000..31af2ac34 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -0,0 +1,37 @@ +package spring.backend.recommendation.infrastructure.clova.dto.request; + +import lombok.Builder; +import lombok.Getter; +import spring.backend.recommendation.dto.request.UserInputRequest; + +import java.util.ArrayList; + +@Builder +@Getter +public class ClovaRequest { + private ArrayList messages; + private double topP; + private int topK; + private int maxTokens; + private double temperature; + private double repeatPenalty; + private boolean includeAiFilters; + private int seed; + + public static ClovaRequest createClovaRequest(UserInputRequest userInputRequest) { + ArrayList messages = new ArrayList<>(); + messages.add(Message.createSystem()); + messages.add(Message.createMessage(userInputRequest)); + + return ClovaRequest.builder() + .messages(messages) + .topP(0.8) + .topK(0) + .maxTokens(500) + .temperature(0.5) + .repeatPenalty(5.0) + .includeAiFilters(true) + .seed(0) + .build(); + } +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/response/ClovaResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/response/ClovaResponse.java new file mode 100644 index 000000000..924c7adea --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/response/ClovaResponse.java @@ -0,0 +1,23 @@ +package spring.backend.recommendation.infrastructure.clova.dto.response; + + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ClovaResponse { + private Result result; + + @Getter + @NoArgsConstructor + public static class Result { + private Message message; + } + + @Getter + @NoArgsConstructor + public static class Message { + private String content; + } +} \ No newline at end of file From f19b1d7533c052ca0a8e8354aa17a55c7dfbe11b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:50:27 +0900 Subject: [PATCH 157/478] =?UTF-8?q?feat:=20(#50)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=20=EC=A0=84=EC=9A=A9=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=A0=20Response=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/ClovaRecommendationResponse.java | 11 +++++++++ .../clova/exception/ClovaErrorCode.java | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java new file mode 100644 index 000000000..3c595fde6 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java @@ -0,0 +1,11 @@ +package spring.backend.recommendation.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ClovaRecommendationResponse { + private Integer order; + private String content; +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java new file mode 100644 index 000000000..dda18be64 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -0,0 +1,24 @@ +package spring.backend.recommendation.infrastructure.clova.exception; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum ClovaErrorCode implements BaseErrorCode { + + NOT_EXIST_LOCATION_WHEN_OFFLINE(HttpStatus.BAD_REQUEST, "오프라인의 경우 위치 정보가 필수입니다."), + NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."); + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From a3abaff7681309b0d19911e731edc32c3b45f4aa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:50:52 +0900 Subject: [PATCH 158/478] =?UTF-8?q?feat:=20(#50)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=20=EC=8A=A4=ED=8A=9C=EB=94=94=EC=98=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=84=20=EB=B3=B4=EB=82=B4=EB=8A=94=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/application/ClovaService.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java new file mode 100644 index 000000000..22e3b9751 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -0,0 +1,56 @@ +package spring.backend.recommendation.infrastructure.clova.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; +import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; + +@Service +@RequiredArgsConstructor +public class ClovaService { + + @Value("${clova.api.url}") + private String apiUrl; + + @Value("${clova.api.api-key}") + private String apiKey; + + @Value("${clova.api.api-gateway-key}") + private String apiGatewayKey; + + private final WebClient webClient; + + public String requestToClovaStudio(UserInputRequest userInputRequest) { + try { + ClovaRequest request = ClovaRequest.createClovaRequest(userInputRequest); + + ClovaResponse result = webClient.post() + .uri(apiUrl) + .headers(httpHeaders -> { + httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); + httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + }) + .body(Mono.just(request), ClovaRequest.class) + .retrieve() + .bodyToMono(ClovaResponse.class) + .block(); + + assert result != null; + + return result.getResult().getMessage().getContent(); + } catch (DomainException e) { + throw e; + } + catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } +} \ No newline at end of file From 3152659375ab81ec845590b1463bd7a72904bed1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 16:51:43 +0900 Subject: [PATCH 159/478] =?UTF-8?q?feat:=20(#50)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=EC=97=90=EA=B2=8C=20=EC=B6=94=EC=B2=9C=EB=82=B4?= =?UTF-8?q?=EC=97=AD=EC=9D=84=20=EB=B0=9B=EB=8A=94=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=99=80=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 32 +++++++++++++++++++ ...estRecommendationsFromClovaController.java | 27 ++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java create mode 100644 src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java new file mode 100644 index 000000000..09076ff36 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -0,0 +1,32 @@ +package spring.backend.recommendation.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.infrastructure.clova.application.ClovaService; +import spring.backend.recommendation.dto.request.UserInputRequest; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class GetRecommendationsFromClovaService { + private final ClovaService clovaService; + + public List getRecommendationsFromClova(UserInputRequest userInputRequest) { + String result = clovaService.requestToClovaStudio(userInputRequest); + String[] recommendations = result.split("\n"); + + List clovaResponses = new ArrayList<>(); + + for(int i = 0; i < recommendations.length; i++) { + ClovaRecommendationResponse clovaResponse = new ClovaRecommendationResponse(i + 1, recommendations[i]); + clovaResponses.add(clovaResponse); + } + + return clovaResponses; + } +} diff --git a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java new file mode 100644 index 000000000..dc4b75a7a --- /dev/null +++ b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java @@ -0,0 +1,27 @@ +package spring.backend.recommendation.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.presentation.RestResponse; +import spring.backend.recommendation.application.GetRecommendationsFromClovaService; +import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/recommendations") +public class RequestRecommendationsFromClovaController { + private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; + + @PostMapping + public ResponseEntity>> requestRecommendations(@RequestBody UserInputRequest userInputRequest) { + List response = getRecommendationsFromClovaService.getRecommendationsFromClova(userInputRequest); + return ResponseEntity.ok(new RestResponse<>(response)); + } +} \ No newline at end of file From aa038e4b188a842767808fb8c9005501b814fa98 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 22:31:31 +0900 Subject: [PATCH 160/478] =?UTF-8?q?feat:=20(#50)=20WebClient=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=99=80=20Clova=20=EA=B4=80=EB=A0=A8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/exception/error/GlobalErrorCode.java | 3 ++- .../infrastructure/clova/exception/ClovaErrorCode.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java index 92a37cab7..de06af23b 100644 --- a/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java +++ b/src/main/java/spring/backend/core/exception/error/GlobalErrorCode.java @@ -10,7 +10,8 @@ public enum GlobalErrorCode implements BaseErrorCode { ALREADY_PROCESS_STARTED(HttpStatus.BAD_REQUEST, "이미 처리중인 요청입니다."), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 내부 오류입니다."), - REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버와의 연결에 문제가 발생했습니다."); + REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버와의 연결에 문제가 발생했습니다."), + WEB_CLIENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "WebClient 오류가 발생했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java index dda18be64..3195a653f 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -12,7 +12,8 @@ public enum ClovaErrorCode implements BaseErrorCode { NOT_EXIST_LOCATION_WHEN_OFFLINE(HttpStatus.BAD_REQUEST, "오프라인의 경우 위치 정보가 필수입니다."), - NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."); + NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."), + NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."); private final HttpStatus httpStatus; private final String message; From 66ba122e4aa24fb57c2e122d287b5f2e4a1e3506 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Oct 2024 22:35:32 +0900 Subject: [PATCH 161/478] =?UTF-8?q?refactor:=20(#50)=20ClovaService=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=ED=95=9C=EB=8B=A4.=20-=20ClovaService=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=A4=EC=A0=95=ED=96=88=EB=8D=98=20WebClient=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=EB=A5=BC=20WebClientConfiguration=20?= =?UTF-8?q?=EC=9D=98=20default=20=ED=97=A4=EB=8D=94=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.=20-=20ClovaService=EC=9D=98=20NULL?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EB=AA=85=ED=99=95=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0,=20body?= =?UTF-8?q?=EB=A5=BC=20bodyValue=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.(=ED=81=B4=EB=A1=9C=EB=B0=94=20=EC=8A=A4=ED=8A=9C?= =?UTF-8?q?=EB=94=94=EC=98=A4=EB=A1=9C=20=EC=9A=94=EC=B2=AD=EC=9D=84=20?= =?UTF-8?q?=EB=B3=B4=EB=82=BC=20=EC=8B=9C=20request=EB=8A=94=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=A0=84=EC=97=90=20=EC=99=84=EC=84=B1=EB=90=98?= =?UTF-8?q?=EC=96=B4=20=EC=9E=88=EC=9C=BC=EB=AF=80=EB=A1=9C=20bodyValue?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webclient/WebClientConfiguration.java | 16 ++++++++++++- .../clova/application/ClovaService.java | 24 +++++++------------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java index 49be9ced1..d87a2868c 100644 --- a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java @@ -1,14 +1,28 @@ package spring.backend.core.configuration.webclient; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class WebClientConfiguration { + @Value("${clova.api.api-key}") + private String apiKey; + + @Value("${clova.api.api-gateway-key}") + private String apiGatewayKey; + @Bean public WebClient webClient(WebClient.Builder builder) { - return builder.build(); + return builder.defaultHeaders( + httpHeaders -> { + httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); + httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + } + ).build(); } } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index 22e3b9751..915066231 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -5,12 +5,14 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; import reactor.core.publisher.Mono; import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.dto.request.UserInputRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Service @RequiredArgsConstructor @@ -19,12 +21,6 @@ public class ClovaService { @Value("${clova.api.url}") private String apiUrl; - @Value("${clova.api.api-key}") - private String apiKey; - - @Value("${clova.api.api-gateway-key}") - private String apiGatewayKey; - private final WebClient webClient; public String requestToClovaStudio(UserInputRequest userInputRequest) { @@ -33,23 +29,21 @@ public String requestToClovaStudio(UserInputRequest userInputRequest) { ClovaResponse result = webClient.post() .uri(apiUrl) - .headers(httpHeaders -> { - httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); - httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); - httpHeaders.setContentType(MediaType.APPLICATION_JSON); - }) - .body(Mono.just(request), ClovaRequest.class) + .bodyValue(request) .retrieve() .bodyToMono(ClovaResponse.class) .block(); - assert result != null; + if (result == null || result.getResult() == null || result.getResult().getMessage() == null) { + throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); + } return result.getResult().getMessage().getContent(); } catch (DomainException e) { throw e; - } - catch (Exception e) { + } catch (WebClientException e) { + throw GlobalErrorCode.WEB_CLIENT_ERROR.toException(); + } catch (Exception e) { throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } From ef1e2b5f2b63d0fa9021f75d21d09d9059c320a3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:15:05 +0900 Subject: [PATCH 162/478] =?UTF-8?q?refactor:=20(#50)=20UserInputRequest?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20ClovaRecommendationRequest?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0,=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 4 ++-- ...putRequest.java => ClovaRecommendationRequest.java} | 10 ++++++---- .../infrastructure/clova/application/ClovaService.java | 6 ++---- .../infrastructure/clova/dto/request/ClovaRequest.java | 4 ++-- .../infrastructure/clova/dto/request/Message.java | 6 +++--- .../RequestRecommendationsFromClovaController.java | 5 +++-- 6 files changed, 18 insertions(+), 17 deletions(-) rename src/main/java/spring/backend/recommendation/dto/request/{UserInputRequest.java => ClovaRecommendationRequest.java} (63%) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 09076ff36..beff959c9 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Service; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.application.ClovaService; -import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import java.util.ArrayList; import java.util.List; @@ -16,7 +16,7 @@ public class GetRecommendationsFromClovaService { private final ClovaService clovaService; - public List getRecommendationsFromClova(UserInputRequest userInputRequest) { + public List getRecommendationsFromClova(ClovaRecommendationRequest userInputRequest) { String result = clovaService.requestToClovaStudio(userInputRequest); String[] recommendations = result.split("\n"); diff --git a/src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java similarity index 63% rename from src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java rename to src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index 9f19c0840..de6248a97 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/UserInputRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -1,18 +1,20 @@ package spring.backend.recommendation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.Getter; import spring.backend.recommendation.infrastructure.clova.domain.value.Type; @Getter -public class UserInputRequest { +public class ClovaRecommendationRequest { @NotNull - @Pattern(regexp = "^(5|[1-9][0-9]|[1-2][0-9]{2}|300)$", message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") + @Min(value = 5, message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") + @Max(value = 300, message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") @Schema(description = "자투리 시간", example = "30") - private int spareTime; + private Integer spareTime; @NotNull @Schema(description = "활동 타입", example = "OFFLINE") diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index 915066231..03b7dd7c9 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -2,15 +2,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; -import reactor.core.publisher.Mono; import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; -import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @@ -23,7 +21,7 @@ public class ClovaService { private final WebClient webClient; - public String requestToClovaStudio(UserInputRequest userInputRequest) { + public String requestToClovaStudio(ClovaRecommendationRequest userInputRequest) { try { ClovaRequest request = ClovaRequest.createClovaRequest(userInputRequest); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index 31af2ac34..9a6dfee8f 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -2,7 +2,7 @@ import lombok.Builder; import lombok.Getter; -import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import java.util.ArrayList; @@ -18,7 +18,7 @@ public class ClovaRequest { private boolean includeAiFilters; private int seed; - public static ClovaRequest createClovaRequest(UserInputRequest userInputRequest) { + public static ClovaRequest createClovaRequest(ClovaRecommendationRequest userInputRequest) { ArrayList messages = new ArrayList<>(); messages.add(Message.createSystem()); messages.add(Message.createMessage(userInputRequest)); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index c6082058f..50588b96f 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -3,7 +3,7 @@ import lombok.Builder; import lombok.Getter; -import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Getter @@ -25,11 +25,11 @@ public static Message createSystem() { .build(); } - public static Message createMessage(UserInputRequest userInputRequest) { + public static Message createMessage(ClovaRecommendationRequest userInputRequest) { return Message.builder().role(ROLE.user).content(createContent(userInputRequest)).build(); } - private static String createContent(UserInputRequest userInputRequest) { + private static String createContent(ClovaRecommendationRequest userInputRequest) { int spareTime = userInputRequest.getSpareTime(); String activityType = userInputRequest.getActivityType().toString(); String keyword = userInputRequest.getKeyword(); diff --git a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java index dc4b75a7a..16eb87d94 100644 --- a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java @@ -1,5 +1,6 @@ package spring.backend.recommendation.presentation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -8,7 +9,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.core.presentation.RestResponse; import spring.backend.recommendation.application.GetRecommendationsFromClovaService; -import spring.backend.recommendation.dto.request.UserInputRequest; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import java.util.List; @@ -20,7 +21,7 @@ public class RequestRecommendationsFromClovaController { private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; @PostMapping - public ResponseEntity>> requestRecommendations(@RequestBody UserInputRequest userInputRequest) { + public ResponseEntity>> requestRecommendations(@Valid @RequestBody ClovaRecommendationRequest userInputRequest) { List response = getRecommendationsFromClovaService.getRecommendationsFromClova(userInputRequest); return ResponseEntity.ok(new RestResponse<>(response)); } From 90de25e4ab7187030764b17e7f0353a3bafb8305 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:26:49 +0900 Subject: [PATCH 163/478] =?UTF-8?q?refactor:=20(#50)=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 4 +++- .../clova/dto/request/ClovaRequest.java | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index beff959c9..51a2f9dcb 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -14,11 +14,13 @@ @Log4j2 @RequiredArgsConstructor public class GetRecommendationsFromClovaService { + private static final String LF = "\n"; + private final ClovaService clovaService; public List getRecommendationsFromClova(ClovaRecommendationRequest userInputRequest) { String result = clovaService.requestToClovaStudio(userInputRequest); - String[] recommendations = result.split("\n"); + String[] recommendations = result.split(LF); List clovaResponses = new ArrayList<>(); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index 9a6dfee8f..a7bd7105e 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -9,6 +9,14 @@ @Builder @Getter public class ClovaRequest { + private static final double DEFAULT_TOP_P = 0.8; + private static final int DEFAULT_TOP_K = 0; + private static final int DEFAULT_MAX_TOKENS = 500; + private static final double DEFAULT_TEMPERATURE = 0.5; + private static final double DEFAULT_REPEAT_PENALTY = 5.0; + private static final boolean DEFAULT_INCLUDE_AI_FILTERS = true; + private static final int DEFAULT_SEED = 0; + private ArrayList messages; private double topP; private int topK; @@ -25,13 +33,13 @@ public static ClovaRequest createClovaRequest(ClovaRecommendationRequest userInp return ClovaRequest.builder() .messages(messages) - .topP(0.8) - .topK(0) - .maxTokens(500) - .temperature(0.5) - .repeatPenalty(5.0) - .includeAiFilters(true) - .seed(0) + .topP(DEFAULT_TOP_P) + .topK(DEFAULT_TOP_K) + .maxTokens(DEFAULT_MAX_TOKENS) + .temperature(DEFAULT_TEMPERATURE) + .repeatPenalty(DEFAULT_REPEAT_PENALTY) + .includeAiFilters(DEFAULT_INCLUDE_AI_FILTERS) + .seed(DEFAULT_SEED) .build(); } } From 80aa5394b858e5e89e06a908071d60c0d3ef75a5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:31:23 +0900 Subject: [PATCH 164/478] =?UTF-8?q?refactor:=20(#50)=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20Enum=EC=9D=84=20=EC=82=AD=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/domain/value/Type.java | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java deleted file mode 100644 index ed40dc5a5..000000000 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/domain/value/Type.java +++ /dev/null @@ -1,10 +0,0 @@ -package spring.backend.recommendation.infrastructure.clova.domain.value; - -import lombok.Getter; - -@Getter -public enum Type { - OFFLINE, - ONLINE, - OFFLINE_AND_ONLINE; -} \ No newline at end of file From 8ba3a290270b849fcbe0235a2f9cfd3bc1ca9fe3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:32:05 +0900 Subject: [PATCH 165/478] =?UTF-8?q?refactor:=20(#50)=20Activity=EC=9D=98?= =?UTF-8?q?=20Enum=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0,=20ClovaRecomm?= =?UTF-8?q?endationRequest=20=ED=83=80=EC=9E=85=EC=9D=98=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ClovaRecommendationRequest.java | 5 +++-- .../clova/application/ClovaService.java | 4 ++-- .../infrastructure/clova/dto/request/Message.java | 15 ++++++++------- ...RequestRecommendationsFromClovaController.java | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index de6248a97..b8f206c2b 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -5,7 +5,8 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Getter; -import spring.backend.recommendation.infrastructure.clova.domain.value.Type; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; @Getter public class ClovaRecommendationRequest { @@ -22,7 +23,7 @@ public class ClovaRecommendationRequest { @NotNull @Schema(description = "활동 키워드", example = "문화/예술") - private String keyword; + private Keyword keyword; @Schema(description = "위치", example = "서울시 강남구") private String location; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index 03b7dd7c9..d52572f8c 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -21,9 +21,9 @@ public class ClovaService { private final WebClient webClient; - public String requestToClovaStudio(ClovaRecommendationRequest userInputRequest) { + public String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { try { - ClovaRequest request = ClovaRequest.createClovaRequest(userInputRequest); + ClovaRequest request = ClovaRequest.createClovaRequest(clovaRecommendationRequest); ClovaResponse result = webClient.post() .uri(apiUrl) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 50588b96f..2ca2d69e6 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -3,6 +3,7 @@ import lombok.Builder; import lombok.Getter; +import spring.backend.activity.domain.value.Keyword; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @@ -25,15 +26,15 @@ public static Message createSystem() { .build(); } - public static Message createMessage(ClovaRecommendationRequest userInputRequest) { - return Message.builder().role(ROLE.user).content(createContent(userInputRequest)).build(); + public static Message createMessage(ClovaRecommendationRequest clovaRecommendationRequest) { + return Message.builder().role(ROLE.user).content(createContent(clovaRecommendationRequest)).build(); } - private static String createContent(ClovaRecommendationRequest userInputRequest) { - int spareTime = userInputRequest.getSpareTime(); - String activityType = userInputRequest.getActivityType().toString(); - String keyword = userInputRequest.getKeyword(); - String location = userInputRequest.getLocation(); + private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { + int spareTime = clovaRecommendationRequest.getSpareTime(); + String activityType = clovaRecommendationRequest.getActivityType().toString(); + Keyword keyword = clovaRecommendationRequest.getKeyword(); + String location = clovaRecommendationRequest.getLocation(); if (activityType.equals("OFFLINE")) { if (location == null) { diff --git a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java index 16eb87d94..d9c23254f 100644 --- a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java @@ -21,8 +21,8 @@ public class RequestRecommendationsFromClovaController { private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; @PostMapping - public ResponseEntity>> requestRecommendations(@Valid @RequestBody ClovaRecommendationRequest userInputRequest) { - List response = getRecommendationsFromClovaService.getRecommendationsFromClova(userInputRequest); + public ResponseEntity>> requestRecommendations(@Valid @RequestBody ClovaRecommendationRequest clovaRecommendationRequest) { + List response = getRecommendationsFromClovaService.getRecommendationsFromClova(clovaRecommendationRequest); return ResponseEntity.ok(new RestResponse<>(response)); } } \ No newline at end of file From 72fd72de25313bcb03725889be8f3fefec0694d3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:36:33 +0900 Subject: [PATCH 166/478] =?UTF-8?q?refactor:=20(#50)=20Keyword=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recommendation/dto/request/ClovaRecommendationRequest.java | 2 +- .../infrastructure/clova/dto/request/Message.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index b8f206c2b..578080ac7 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -23,7 +23,7 @@ public class ClovaRecommendationRequest { @NotNull @Schema(description = "활동 키워드", example = "문화/예술") - private Keyword keyword; + private Keyword.Category keyword; @Schema(description = "위치", example = "서울시 강남구") private String location; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 2ca2d69e6..416d12f5e 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -33,7 +33,7 @@ public static Message createMessage(ClovaRecommendationRequest clovaRecommendati private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { int spareTime = clovaRecommendationRequest.getSpareTime(); String activityType = clovaRecommendationRequest.getActivityType().toString(); - Keyword keyword = clovaRecommendationRequest.getKeyword(); + Keyword.Category keyword = clovaRecommendationRequest.getKeyword(); String location = clovaRecommendationRequest.getLocation(); if (activityType.equals("OFFLINE")) { From 15c54f7b365f1db4408a9e644eab3e6f59ceda35 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 29 Oct 2024 14:48:38 +0900 Subject: [PATCH 167/478] =?UTF-8?q?refactor:=20(#50)=20=EC=97=B4=EA=B1=B0?= =?UTF-8?q?=20=EC=83=81=EC=88=98=EB=A5=BC=20=EB=8C=80=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/dto/request/Message.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 416d12f5e..55ce9e9e5 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -15,19 +15,19 @@ public class Message { private String content; public enum ROLE { - system, user + SYSTEM, USER } public static Message createSystem() { String content = "너는 자투리시간과 활동의 타입(온라인,오프라인, 둘다)를 받고, 키워드와 위치 정보를 받아 사용자에게 5가지의 활동을 추천해주는 봇이야. 추천목록만 보내주면 되고, 각 추천목록은 줄바꿈으로 구분해줘. 추천목록을 제외한 잡설은 보내지마."; return Message.builder() - .role(ROLE.system) + .role(ROLE.SYSTEM) .content(content) .build(); } public static Message createMessage(ClovaRecommendationRequest clovaRecommendationRequest) { - return Message.builder().role(ROLE.user).content(createContent(clovaRecommendationRequest)).build(); + return Message.builder().role(ROLE.USER).content(createContent(clovaRecommendationRequest)).build(); } private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { From 878e81d6649c095829a22dfee2330a91849b2313 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 12:31:23 +0900 Subject: [PATCH 168/478] =?UTF-8?q?refactor:=20(#50)=20WebClientConfigurat?= =?UTF-8?q?ion=EC=9D=84=20ClovaStudio=20=EC=A0=84=EC=9A=A9=20=EC=9B=B9?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webclient/WebClientConfiguration.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java index d87a2868c..efeeecda7 100644 --- a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java @@ -16,13 +16,13 @@ public class WebClientConfiguration { private String apiGatewayKey; @Bean - public WebClient webClient(WebClient.Builder builder) { - return builder.defaultHeaders( - httpHeaders -> { + public WebClient clovaStudioWebClient() { + return WebClient.builder() + .defaultHeaders(httpHeaders -> { httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); httpHeaders.setContentType(MediaType.APPLICATION_JSON); - } - ).build(); + }) + .build(); } } From 7b88369b9906ed60c60945fc13464a6be623b460 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 12:33:31 +0900 Subject: [PATCH 169/478] =?UTF-8?q?refactor:=20(#50)=20Clova=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 23 ++++++++++++++----- .../clova/application/ClovaService.java | 21 ++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 51a2f9dcb..874125a79 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -6,6 +6,8 @@ import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.application.ClovaService; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import java.util.ArrayList; import java.util.List; @@ -18,15 +20,24 @@ public class GetRecommendationsFromClovaService { private final ClovaService clovaService; - public List getRecommendationsFromClova(ClovaRecommendationRequest userInputRequest) { - String result = clovaService.requestToClovaStudio(userInputRequest); - String[] recommendations = result.split(LF); + public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { + ClovaResponse result = clovaService.requestToClovaStudio(clovaRecommendationRequest); + + if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { + log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); + throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); + } + + return parseRecommendationsFromClova(result.getResult().getMessage().getContent()); + } + + private List parseRecommendationsFromClova(String content) { + String[] recommendations = content.split(LF); List clovaResponses = new ArrayList<>(); - for(int i = 0; i < recommendations.length; i++) { - ClovaRecommendationResponse clovaResponse = new ClovaRecommendationResponse(i + 1, recommendations[i]); - clovaResponses.add(clovaResponse); + for (int i = 0; i < recommendations.length; i++) { + clovaResponses.add(new ClovaRecommendationResponse(i + 1, recommendations[i])); } return clovaResponses; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index d52572f8c..8b7959efb 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -1,47 +1,44 @@ package spring.backend.recommendation.infrastructure.clova.application; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Service @RequiredArgsConstructor +@Log4j2 public class ClovaService { @Value("${clova.api.url}") private String apiUrl; - private final WebClient webClient; + private final WebClient clovaStudioWebClient; - public String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { + public ClovaResponse requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { try { ClovaRequest request = ClovaRequest.createClovaRequest(clovaRecommendationRequest); - ClovaResponse result = webClient.post() + return clovaStudioWebClient.post() .uri(apiUrl) .bodyValue(request) .retrieve() .bodyToMono(ClovaResponse.class) .block(); - - if (result == null || result.getResult() == null || result.getResult().getMessage() == null) { - throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); - } - - return result.getResult().getMessage().getContent(); - } catch (DomainException e) { - throw e; } catch (WebClientException e) { + log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e); throw GlobalErrorCode.WEB_CLIENT_ERROR.toException(); } catch (Exception e) { + log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } From dbcc497615b09ccc5dd46e14c3aff35c2e1d2e75 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 12:34:36 +0900 Subject: [PATCH 170/478] =?UTF-8?q?refactor:=20(#50)=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=EC=9D=98=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EB=AA=85=EA=B3=BC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/ClovaRequest.java | 4 +-- .../clova/dto/request/Message.java | 36 ++++++++++++------- ...etRecommendationsFromClovaController.java} | 2 +- 3 files changed, 26 insertions(+), 16 deletions(-) rename src/main/java/spring/backend/recommendation/presentation/{RequestRecommendationsFromClovaController.java => GetRecommendationsFromClovaController.java} (95%) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index a7bd7105e..ec14adf14 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -26,10 +26,10 @@ public class ClovaRequest { private boolean includeAiFilters; private int seed; - public static ClovaRequest createClovaRequest(ClovaRecommendationRequest userInputRequest) { + public static ClovaRequest createClovaRequest(ClovaRecommendationRequest clovaRecommendationRequest) { ArrayList messages = new ArrayList<>(); messages.add(Message.createSystem()); - messages.add(Message.createMessage(userInputRequest)); + messages.add(Message.createMessage(clovaRecommendationRequest)); return ClovaRequest.builder() .messages(messages) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 55ce9e9e5..09318c5ae 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -3,7 +3,9 @@ import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @@ -11,38 +13,46 @@ @Builder public class Message { - private ROLE role; + private static final String DEFAULT_SYSTEM_CONTENT = "너는 자투리시간과 활동의 타입(온라인,오프라인, 둘다)를 받고, 키워드와 위치 정보를 받아 사용자에게 5가지의 활동을 추천해주는 봇이야. 추천목록만 보내주면 되고, 각 추천목록은 줄바꿈으로 구분해줘. 추천목록을 제외한 잡설은 보내지마."; + + private String role; private String content; + @RequiredArgsConstructor + @Getter public enum ROLE { - SYSTEM, USER + SYSTEM("system"), USER("user"); + private final String description; } public static Message createSystem() { - String content = "너는 자투리시간과 활동의 타입(온라인,오프라인, 둘다)를 받고, 키워드와 위치 정보를 받아 사용자에게 5가지의 활동을 추천해주는 봇이야. 추천목록만 보내주면 되고, 각 추천목록은 줄바꿈으로 구분해줘. 추천목록을 제외한 잡설은 보내지마."; return Message.builder() - .role(ROLE.SYSTEM) - .content(content) + .role(ROLE.SYSTEM.getDescription()) + .content(DEFAULT_SYSTEM_CONTENT) .build(); } public static Message createMessage(ClovaRecommendationRequest clovaRecommendationRequest) { - return Message.builder().role(ROLE.USER).content(createContent(clovaRecommendationRequest)).build(); + return Message.builder().role(ROLE.USER.getDescription()).content(createContent(clovaRecommendationRequest)).build(); } private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { int spareTime = clovaRecommendationRequest.getSpareTime(); - String activityType = clovaRecommendationRequest.getActivityType().toString(); + Type activityType = clovaRecommendationRequest.getActivityType(); Keyword.Category keyword = clovaRecommendationRequest.getKeyword(); String location = clovaRecommendationRequest.getLocation(); - if (activityType.equals("OFFLINE")) { - if (location == null) { - throw ClovaErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); - } - return "자투리 시간: " + spareTime + "분\\n선호활동: " + activityType + "\\n활동 키워드: " + keyword + "\\n위치 : " + location + "\\n \\n활동 추천해줘\\n\\n"; + if (isActivityTypeOffline(activityType, location)) { + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keyword, location); } else { - return "자투리 시간: " + spareTime + "분\\n선호활동: " + activityType + "\\n활동 키워드: " + keyword + "\\n \\n활동 추천해줘\\n\\n"; + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keyword); + } + } + + private static boolean isActivityTypeOffline(Type activityType, String location) { + if (activityType.equals(Type.OFFLINE) && location == null) { + throw ClovaErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); } + return activityType.equals(Type.OFFLINE); } } \ No newline at end of file diff --git a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java similarity index 95% rename from src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java rename to src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java index d9c23254f..b7055849c 100644 --- a/src/main/java/spring/backend/recommendation/presentation/RequestRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java @@ -17,7 +17,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/recommendations") -public class RequestRecommendationsFromClovaController { +public class GetRecommendationsFromClovaController { private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; @PostMapping From aed2938b3590d44cf5a1c47f8768da7609c1a071 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 13:14:39 +0900 Subject: [PATCH 171/478] =?UTF-8?q?refactor:=20(#50)=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=98=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20DIP=EC=99=80=20DDD=20=EC=9B=90=EC=B9=99=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EA=B0=9C=EC=84=A0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 17 ++--------- .../application/RecommendationProvider.java | 7 +++++ .../ClovaRecommendationProvider.java | 29 +++++++++++++++++++ .../clova/application/ClovaService.java | 3 -- 4 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 src/main/java/spring/backend/recommendation/application/RecommendationProvider.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 874125a79..f9cdf852e 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -4,10 +4,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; -import spring.backend.recommendation.infrastructure.clova.application.ClovaService; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; -import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; -import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import java.util.ArrayList; import java.util.List; @@ -18,21 +15,11 @@ public class GetRecommendationsFromClovaService { private static final String LF = "\n"; - private final ClovaService clovaService; + private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { - ClovaResponse result = clovaService.requestToClovaStudio(clovaRecommendationRequest); - if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { - log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); - throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); - } - - return parseRecommendationsFromClova(result.getResult().getMessage().getContent()); - } - - private List parseRecommendationsFromClova(String content) { - String[] recommendations = content.split(LF); + String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LF); List clovaResponses = new ArrayList<>(); diff --git a/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java new file mode 100644 index 000000000..f7adac481 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java @@ -0,0 +1,7 @@ +package spring.backend.recommendation.application; + +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; + +public interface RecommendationProvider { + String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest); +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java new file mode 100644 index 000000000..eab512c32 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -0,0 +1,29 @@ +package spring.backend.recommendation.infrastructure.clova.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.application.RecommendationProvider; +import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class ClovaRecommendationProvider implements RecommendationProvider { + + private final ClovaService clovaService; + + @Override + public String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { + ClovaResponse result = clovaService.requestToClovaStudio(clovaRecommendationRequest); + + if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { + log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); + throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); + } + + return result.getResult().getMessage().getContent(); + } +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index 8b7959efb..10ba8bf81 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -3,16 +3,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; -import spring.backend.core.exception.DomainException; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; -import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Service @RequiredArgsConstructor From a779e6a99186ca8c7407ff46f5a711c8585a5bbf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 13:38:11 +0900 Subject: [PATCH 172/478] =?UTF-8?q?refactor:=20(#50)=20=EC=9E=90=ED=88=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EA=B0=84=EC=9D=80=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?10=EB=B6=84=EB=B6=80=ED=84=B0=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ClovaRecommendationRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index 578080ac7..7ac8c71b8 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -12,8 +12,8 @@ public class ClovaRecommendationRequest { @NotNull - @Min(value = 5, message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") - @Max(value = 300, message = "자투리 시간은 5부터 300 사이의 숫자로 입력해주세요.") + @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") + @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") @Schema(description = "자투리 시간", example = "30") private Integer spareTime; From e83f90afa2ff99521f4b823f677f68dd9b328be9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 13:39:24 +0900 Subject: [PATCH 173/478] =?UTF-8?q?refactor:=20(#50)=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20LF=20->=20LINE=5FSEPA?= =?UTF-8?q?RATOR=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index f9cdf852e..659a8a533 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -13,13 +13,13 @@ @Log4j2 @RequiredArgsConstructor public class GetRecommendationsFromClovaService { - private static final String LF = "\n"; + private static final String LINE_SEPARATOR = "\n"; private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { - String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LF); + String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); List clovaResponses = new ArrayList<>(); From df9263eb2f4bdb72d08eeb1f6b09455e627facb8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 13:40:17 +0900 Subject: [PATCH 174/478] =?UTF-8?q?refactor:=20(#50)=20Enum=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20ROLE=20->=20Role=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/dto/request/Message.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 09318c5ae..311544fe1 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -20,20 +20,20 @@ public class Message { @RequiredArgsConstructor @Getter - public enum ROLE { + public enum Role { SYSTEM("system"), USER("user"); private final String description; } public static Message createSystem() { return Message.builder() - .role(ROLE.SYSTEM.getDescription()) + .role(Role.SYSTEM.getDescription()) .content(DEFAULT_SYSTEM_CONTENT) .build(); } public static Message createMessage(ClovaRecommendationRequest clovaRecommendationRequest) { - return Message.builder().role(ROLE.USER.getDescription()).content(createContent(clovaRecommendationRequest)).build(); + return Message.builder().role(Role.USER.getDescription()).content(createContent(clovaRecommendationRequest)).build(); } private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { From e2d376956a59476a6f8da2bd01b1c5a82f299815 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 16:24:07 +0900 Subject: [PATCH 175/478] =?UTF-8?q?feat:=20(#50)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=EB=A5=BC=20=EC=97=AC=EB=9F=AC?= =?UTF-8?q?=EA=B0=9C=20=EB=B0=9B=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ClovaRecommendationRequest.java | 2 +- .../infrastructure/clova/dto/request/Message.java | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index 7ac8c71b8..cf03c111f 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -23,7 +23,7 @@ public class ClovaRecommendationRequest { @NotNull @Schema(description = "활동 키워드", example = "문화/예술") - private Keyword.Category keyword; + private Keyword.Category[] keywords; @Schema(description = "위치", example = "서울시 강남구") private String location; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 311544fe1..d75632597 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -9,6 +9,9 @@ import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; +import java.util.Arrays; +import java.util.stream.Collectors; + @Getter @Builder public class Message { @@ -39,13 +42,15 @@ public static Message createMessage(ClovaRecommendationRequest clovaRecommendati private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { int spareTime = clovaRecommendationRequest.getSpareTime(); Type activityType = clovaRecommendationRequest.getActivityType(); - Keyword.Category keyword = clovaRecommendationRequest.getKeyword(); + String keywords = Arrays.stream(clovaRecommendationRequest.getKeywords()) + .map(Keyword.Category::getDescription) + .collect(Collectors.joining(", ")); String location = clovaRecommendationRequest.getLocation(); if (isActivityTypeOffline(activityType, location)) { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keyword, location); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keywords, location); } else { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keyword); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keywords); } } From 740953ae83ad581fdf08938e8328791012d65b77 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 16:38:27 +0900 Subject: [PATCH 176/478] =?UTF-8?q?feat:=20(#50)=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=84=A0=ED=83=9D=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20=EB=9E=9C=EB=8D=A4?= =?UTF-8?q?=ED=95=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=A1=9C=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=EC=9D=B4=20=EC=B6=94=EC=B2=9C=EB=90=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ClovaRecommendationRequest.java | 2 +- .../clova/dto/request/Message.java | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index cf03c111f..b73b0061c 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -22,7 +22,7 @@ public class ClovaRecommendationRequest { private Type activityType; @NotNull - @Schema(description = "활동 키워드", example = "문화/예술") + @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") private Keyword.Category[] keywords; @Schema(description = "위치", example = "서울시 강남구") diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index d75632597..5c6adddff 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -42,9 +42,7 @@ public static Message createMessage(ClovaRecommendationRequest clovaRecommendati private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { int spareTime = clovaRecommendationRequest.getSpareTime(); Type activityType = clovaRecommendationRequest.getActivityType(); - String keywords = Arrays.stream(clovaRecommendationRequest.getKeywords()) - .map(Keyword.Category::getDescription) - .collect(Collectors.joining(", ")); + String keywords = parseKeywords(clovaRecommendationRequest.getKeywords()); String location = clovaRecommendationRequest.getLocation(); if (isActivityTypeOffline(activityType, location)) { @@ -60,4 +58,16 @@ private static boolean isActivityTypeOffline(Type activityType, String location) } return activityType.equals(Type.OFFLINE); } + + private static String parseKeywords(Keyword.Category[] keywords) { + if (keywords.length == 0) { + return Arrays.stream(Keyword.Category.values()) + .map(Keyword.Category::getDescription) + .collect(Collectors.joining(", ")); + } else { + return Arrays.stream(keywords) + .map(Keyword.Category::getDescription) + .collect(Collectors.joining(", ")); + } + } } \ No newline at end of file From d7754c7d11da061bb0a1b80d002cd577dd5c889e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Oct 2024 16:41:07 +0900 Subject: [PATCH 177/478] =?UTF-8?q?refactor:=20(#50)=20Provider=EC=9D=98?= =?UTF-8?q?=20Service=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=84=20Component=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/application/ClovaRecommendationProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index eab512c32..aa4a67ee3 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -2,13 +2,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.application.RecommendationProvider; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; -@Service +@Component @RequiredArgsConstructor @Log4j2 public class ClovaRecommendationProvider implements RecommendationProvider { From 34fd66bbdbe5930ba39eee279165d215df2cd9b6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 31 Oct 2024 01:23:27 +0900 Subject: [PATCH 178/478] =?UTF-8?q?refactor:=20(#50)=20Type=EC=9D=98=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/spring/backend/activity/domain/value/Type.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/domain/value/Type.java b/src/main/java/spring/backend/activity/domain/value/Type.java index e872ed3cb..41b84cb1e 100644 --- a/src/main/java/spring/backend/activity/domain/value/Type.java +++ b/src/main/java/spring/backend/activity/domain/value/Type.java @@ -8,7 +8,7 @@ public enum Type { ONLINE("온라인"), OFFLINE("오프라인"), - ONLINE_AND_OFFLINE("둘 다"); + ONLINE_AND_OFFLINE("온라인과 오프라인 모두"); private final String description; } From a6f9b21232777f5dc0959c23a35983f120d694cf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 31 Oct 2024 01:23:53 +0900 Subject: [PATCH 179/478] =?UTF-8?q?refactor:=20(#50)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=97=90=EA=B2=8C=20=EC=A4=84=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 18 ++++++++++++++++-- .../response/ClovaRecommendationResponse.java | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 659a8a533..9138f8e8a 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -14,7 +14,6 @@ @RequiredArgsConstructor public class GetRecommendationsFromClovaService { private static final String LINE_SEPARATOR = "\n"; - private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { @@ -23,8 +22,23 @@ public List getRecommendationsFromClova(ClovaRecomm List clovaResponses = new ArrayList<>(); + int order = 1; + for (int i = 0; i < recommendations.length; i++) { - clovaResponses.add(new ClovaRecommendationResponse(i + 1, recommendations[i])); + String line = recommendations[i].trim(); + + if (line.matches("^\\d+\\. title :.*")) { + String title = line.replaceFirst("^\\d+\\. title :", "").trim(); + String content = ""; + + if (i + 1 < recommendations.length && recommendations[i + 1].trim().startsWith("content :")) { + content = recommendations[i + 1].trim().replaceFirst("^content :", "").trim(); + i++; + } + + clovaResponses.add(new ClovaRecommendationResponse(order, title, content)); + order++; + } } return clovaResponses; diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java index 3c595fde6..b62f19462 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java @@ -7,5 +7,6 @@ @AllArgsConstructor public class ClovaRecommendationResponse { private Integer order; + private String title; private String content; } From 7b02853cc4d910c81c5b861c959df6b2447a494f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 31 Oct 2024 01:24:21 +0900 Subject: [PATCH 180/478] =?UTF-8?q?refactor:=20(#50)=20ClovaStudio?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EB=82=BC=20AI=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/Message.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 5c6adddff..4dceedeed 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -16,7 +16,21 @@ @Builder public class Message { - private static final String DEFAULT_SYSTEM_CONTENT = "너는 자투리시간과 활동의 타입(온라인,오프라인, 둘다)를 받고, 키워드와 위치 정보를 받아 사용자에게 5가지의 활동을 추천해주는 봇이야. 추천목록만 보내주면 되고, 각 추천목록은 줄바꿈으로 구분해줘. 추천목록을 제외한 잡설은 보내지마."; + private static final String DEFAULT_SYSTEM_CONTENT = "사용자에게 자투리 시간 , 원하는 활동 타입, 활동 키워드, 위치를 입력받은 뒤 입력받은 값들을 고려해 활동을 추천해줘.\n" + + "\n" + + "예를 들어 입력으로 \n" + + "자투리 시간 : 20분 , 선호활동 : ONLINE , 활동 키워드: RELAXATION,\n" + + "\n" + + "답변은 다음과 같은 형식으로 해줘.\n" + + "\n" + + "title : 마음의 편안을 가져다주는 명상 음악 20분 듣기\n" + + "content: 휴식에는 역시 명상이 최고!\n" + + "\n" + + "답변 예시와 비슷한 형태로 5가지의 활동을 추천해줘.\n" + + "\n" + + "답변의 형식을 꼭 지켜줘\n" + + "title : string \n" + + "content : string"; private String role; private String content; @@ -45,18 +59,18 @@ private static String createContent(ClovaRecommendationRequest clovaRecommendati String keywords = parseKeywords(clovaRecommendationRequest.getKeywords()); String location = clovaRecommendationRequest.getLocation(); - if (isActivityTypeOffline(activityType, location)) { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keywords, location); + if (isActivityTypeOfflineOrOnlineAndOffline(activityType, location)) { + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); } else { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType, keywords); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); } } - private static boolean isActivityTypeOffline(Type activityType, String location) { - if (activityType.equals(Type.OFFLINE) && location == null) { + private static boolean isActivityTypeOfflineOrOnlineAndOffline(Type activityType, String location) { + if (activityType.equals(Type.OFFLINE) && location == null || activityType.equals(Type.ONLINE_AND_OFFLINE) && location == null) { throw ClovaErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); } - return activityType.equals(Type.OFFLINE); + return activityType.equals(Type.OFFLINE) || activityType.equals(Type.ONLINE_AND_OFFLINE); } private static String parseKeywords(Keyword.Category[] keywords) { From f5011f0ad18345cd135dd268c4e822477118ee12 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 31 Oct 2024 13:35:47 +0900 Subject: [PATCH 181/478] =?UTF-8?q?refactor:=20(#50)=20regex=20pattern()?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=EB=B3=B5=20=EC=83=9D=EC=84=B1=EC=9D=84=20?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=EA=B3=A0=20=EC=83=81=EC=88=98=ED=99=94?= =?UTF-8?q?=EC=8B=9C=EC=BC=9C=20=EC=B5=9C=EC=A0=81=ED=99=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 9138f8e8a..40ab58746 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -8,11 +8,15 @@ import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; @Service @Log4j2 @RequiredArgsConstructor public class GetRecommendationsFromClovaService { + private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile("^\\d+\\. title :.*"); + private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile("^\\d+\\. title :"); + private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile("^content :"); private static final String LINE_SEPARATOR = "\n"; private final RecommendationProvider recommendationProvider; @@ -27,12 +31,12 @@ public List getRecommendationsFromClova(ClovaRecomm for (int i = 0; i < recommendations.length; i++) { String line = recommendations[i].trim(); - if (line.matches("^\\d+\\. title :.*")) { - String title = line.replaceFirst("^\\d+\\. title :", "").trim(); + if (TITLE_FULL_LINE_PATTERN.matcher(line).matches()) { + String title = TITLE_PREFIX_PATTERN.matcher(line).replaceFirst("").trim(); String content = ""; - if (i + 1 < recommendations.length && recommendations[i + 1].trim().startsWith("content :")) { - content = recommendations[i + 1].trim().replaceFirst("^content :", "").trim(); + if (i + 1 < recommendations.length && CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { + content = CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); i++; } From 5a592f3858def8004fcd72d600a263230a43980f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 31 Oct 2024 16:36:07 +0900 Subject: [PATCH 182/478] =?UTF-8?q?refactor:=20(#50)=20=EC=A0=95=EA=B7=9C?= =?UTF-8?q?=ED=91=9C=ED=98=84=EC=8B=9D=EC=9D=84=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 40ab58746..d0251ed77 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -14,12 +14,11 @@ @Log4j2 @RequiredArgsConstructor public class GetRecommendationsFromClovaService { - private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile("^\\d+\\. title :.*"); - private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile("^\\d+\\. title :"); - private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile("^content :"); + private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title :.*"); + private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title :"); + private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); private static final String LINE_SEPARATOR = "\n"; private final RecommendationProvider recommendationProvider; - public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); From 5872c70250edc5dcef90e6f12448cd7f43659ba8 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 1 Nov 2024 00:57:04 +0900 Subject: [PATCH 183/478] =?UTF-8?q?fix:=20(#66)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=EC=97=90=EC=84=9C=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=84=20LocalTime=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/domain/entity/QuickStart.java | 8 ++++---- .../activity/dto/request/QuickStartRequest.java | 6 +++--- .../activity/dto/response/QuickStartResponse.java | 6 +++--- .../persistence/jpa/entity/QuickStartJpaEntity.java | 4 ++-- .../application/CreateQuickStartServiceTest.java | 7 ++++--- .../domain/repository/QuickStartRepositoryTest.java | 4 ++-- .../activity/dto/request/QuickStartRequestTest.java | 10 +++++----- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java index 6f9411b78..75494b5cc 100644 --- a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java +++ b/src/main/java/spring/backend/activity/domain/entity/QuickStart.java @@ -4,8 +4,8 @@ import lombok.Getter; import spring.backend.activity.domain.value.Type; -import java.sql.Time; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.UUID; @Getter @@ -18,7 +18,7 @@ public class QuickStart { private String name; - private Time startTime; + private LocalTime startTime; private Integer spareTime; @@ -30,7 +30,7 @@ public class QuickStart { private Boolean deleted; - public static QuickStart create(UUID memberId, String name, Time startTime, Integer spareTime, Type type) { + public static QuickStart create(UUID memberId, String name, LocalTime startTime, Integer spareTime, Type type) { return QuickStart.builder() .memberId(memberId) .name(name) @@ -40,7 +40,7 @@ public static QuickStart create(UUID memberId, String name, Time startTime, Inte .build(); } - public void update(String name, Time startTime, Integer spareTime, Type type) { + public void update(String name, LocalTime startTime, Integer spareTime, Type type) { this.name = name; this.startTime = startTime; this.spareTime = spareTime; diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java index 5fc56c165..d79afb320 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.*; import spring.backend.activity.domain.value.Type; -import java.sql.Time; +import java.time.LocalTime; public record QuickStartRequest( @@ -15,8 +15,8 @@ public record QuickStartRequest( String name, @NotNull(message = "시작 시간은 필수 입력 항목입니다.") - @Schema(description = "시작 시간", example = "12:30:00") - Time startTime, + @Schema(description = "시작 시간", example = "12:30") + LocalTime startTime, @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java index f86945b4d..d33147a99 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Type; -import java.sql.Time; +import java.time.LocalTime; public record QuickStartResponse( @@ -13,8 +13,8 @@ public record QuickStartResponse( @Schema(description = "빠른 시작 이름", example = "등교") String name, - @Schema(description = "시작 시간", example = "12:30:00") - Time startTime, + @Schema(description = "시작 시간", example = "12:30") + LocalTime startTime, @Schema(description = "자투리 시간", example = "300") Integer spareTime, diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java index eef5db39c..d46d7ec54 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java @@ -11,7 +11,7 @@ import spring.backend.activity.domain.value.Type; import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; -import java.sql.Time; +import java.time.LocalTime; import java.util.UUID; @Entity @@ -25,7 +25,7 @@ public class QuickStartJpaEntity extends BaseLongIdEntity { private String name; - private Time startTime; + private LocalTime startTime; private Integer spareTime; diff --git a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java index a711c1538..0b105efa0 100644 --- a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java +++ b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java @@ -16,12 +16,13 @@ import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; -import java.sql.Time; +import java.time.LocalTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class CreateQuickStartServiceTest { @@ -46,7 +47,7 @@ public void setUp() { .build(); request = new QuickStartRequest( "등교", - Time.valueOf("12:30:00"), + LocalTime.of(12, 30), 300, Type.ONLINE ); diff --git a/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java b/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java index 87b0c87ac..bcaa19630 100644 --- a/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java +++ b/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java @@ -7,8 +7,8 @@ import spring.backend.activity.domain.entity.QuickStart; import spring.backend.activity.domain.value.Type; -import java.sql.Time; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -26,7 +26,7 @@ void setUp() { quickStart = QuickStart.builder() .memberId(UUID.randomUUID()) .name("Test QuickStart") - .startTime(Time.valueOf("01:02:03")) + .startTime(LocalTime.of(12, 30)) .spareTime(60) .type(Type.ONLINE) .createdAt(LocalDateTime.now()) diff --git a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java index 33e8a36e6..4b42f36f8 100644 --- a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java +++ b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource; import spring.backend.activity.domain.value.Type; -import java.sql.Time; +import java.time.LocalTime; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +33,7 @@ class NameValidationTests { @Test @DisplayName("null 값일 경우 에러가 발생한다.") void whenNameIsNull_thenValidationFails() { - QuickStartRequest request = new QuickStartRequest(null, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(null, LocalTime.now(), 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); @@ -44,7 +44,7 @@ void whenNameIsNull_thenValidationFails() { @DisplayName("올바른 형식의 이름일 경우 성공한다.") @ValueSource(strings = {"등교", "이름테스트", "John Doe", "사용자1"}) void whenNameIsValid_thenValidationSucceeds(String name) { - QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isEmpty(); @@ -54,7 +54,7 @@ void whenNameIsValid_thenValidationSucceeds(String name) { @DisplayName("형식에 맞지 않는 이름일 경우 에러가 발생한다.") @ValueSource(strings = {" 이름", "이름 ", "이름@이름", "공백 공백"}) void whenNameIsInvalid_thenValidationFails(String name) { - QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); @@ -65,7 +65,7 @@ void whenNameIsInvalid_thenValidationFails(String name) { @DisplayName("10자를 초과하는 경우 에러가 발생한다.") void whenNameExceedsMaxLength_thenValidationFails() { String name = "매우몹시너무긴이름longname"; - QuickStartRequest request = new QuickStartRequest(name, Time.valueOf("12:30:00"), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); From 4177e62ea6391387233136b84b00a544524ba9f1 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 1 Nov 2024 02:33:19 +0900 Subject: [PATCH 184/478] =?UTF-8?q?feat:=20(#67)=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=9D=84=20=EC=99=84=EB=A3=8C=ED=95=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=B8=20AuthorizedMember=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EA=B3=BC=20argume?= =?UTF-8?q?ntresolver=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/WebMvcConfiguration.java | 4 +++ .../argumentresolver/AuthorizedMember.java | 11 +++++++ .../AuthorizedMemberArgumentResolver.java | 32 +++++++++++++++++++ .../member/exception/MemberErrorCode.java | 3 +- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMember.java create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolver.java diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index b0297114e..5cfd8d200 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -6,6 +6,7 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import spring.backend.core.configuration.argumentresolver.AuthorizedMemberArgumentResolver; import spring.backend.core.configuration.argumentresolver.LoginMemberArgumentResolver; import spring.backend.core.configuration.interceptor.AuthorizationInterceptor; @@ -19,6 +20,8 @@ public class WebMvcConfiguration implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AuthorizedMemberArgumentResolver authorizedMemberArgumentResolver; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -36,5 +39,6 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginMemberArgumentResolver); + resolvers.add(authorizedMemberArgumentResolver); } } diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMember.java b/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMember.java new file mode 100644 index 000000000..2d3ea6494 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMember.java @@ -0,0 +1,11 @@ +package spring.backend.core.configuration.argumentresolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthorizedMember { +} diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolver.java new file mode 100644 index 000000000..1cbb7e397 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolver.java @@ -0,0 +1,32 @@ +package spring.backend.core.configuration.argumentresolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.exception.MemberErrorCode; + +@Component +@RequiredArgsConstructor +public class AuthorizedMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthorizedMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Member authorizedMember = (Member) loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + if (authorizedMember == null || !authorizedMember.isMember()) { + throw MemberErrorCode.NOT_AUTHORIZED_MEMBER.toException(); + } + return authorizedMember; + } +} diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index 83e09765e..7fd3cf93b 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -18,7 +18,8 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_EXIST_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임은 필수 입력값입니다."), INVALID_NICKNAME_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 1자에서 6자 사이여야 합니다."), INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영문, 숫자 조합이어야 합니다."), - ALREADY_REGISTERED_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임이 이미 사용 중입니다."); + ALREADY_REGISTERED_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임이 이미 사용 중입니다."), + NOT_AUTHORIZED_MEMBER(HttpStatus.FORBIDDEN, "회원가입을 완료한 사용자가 아닙니다."); private final HttpStatus httpStatus; From 2efc8d28ec120bf7dc94509425e3464f1cedc73f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 1 Nov 2024 02:33:44 +0900 Subject: [PATCH 185/478] =?UTF-8?q?feat:=20(#67)=20AuthorizedMemberArgumen?= =?UTF-8?q?t=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthorizedMemberArgumentResolverTest.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/test/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolverTest.java diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolverTest.java new file mode 100644 index 000000000..da6f0eed5 --- /dev/null +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/AuthorizedMemberArgumentResolverTest.java @@ -0,0 +1,90 @@ +package spring.backend.core.configuration.argumentresolver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Role; +import spring.backend.member.exception.MemberErrorCode; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AuthorizedMemberArgumentResolverTest { + + @InjectMocks + private AuthorizedMemberArgumentResolver authorizedMemberArgumentResolver; + + @Mock + private LoginMemberArgumentResolver loginMemberArgumentResolver; + + @Mock + private NativeWebRequest webRequest; + + @Mock + private ModelAndViewContainer mavContainer; + + private Member member; + private Member guest; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + member = Member.builder() + .id(UUID.randomUUID()) + .role(Role.MEMBER) + .build(); + guest = Member.builder() + .id(UUID.randomUUID()) + .role(Role.GUEST) + .build(); + } + + @DisplayName("AuthorizedMember 어노테이션이 있는 경우 지원한다") + @Test + public void supportsParameterReturnsTrueForLoginMember() { + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.hasParameterAnnotation(AuthorizedMember.class)).thenReturn(true); + Assertions.assertTrue(authorizedMemberArgumentResolver.supportsParameter(parameter)); + } + + @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 AuthorizedMember 객체를 반환한다") + @Test + public void returnsAuthorizedMemberObject_whenAuthorizationHeaderIsProvided() throws Exception { + // when + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.hasParameterAnnotation(AuthorizedMember.class)).thenReturn(true); + when(loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null)).thenReturn(member); + + // then + Object result = authorizedMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); + assertNotNull(result); + assertThat(result).isEqualTo(member); + } + + @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 Guest인 경우 예외를 발생시킨다") + @Test + public void throwsNotAuthorizedMemberException_whenGuestMemberIsProvided() throws Exception { + // when + MethodParameter parameter = mock(MethodParameter.class); + when(parameter.hasParameterAnnotation(AuthorizedMember.class)).thenReturn(true); + when(loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null)).thenReturn(guest); + + // then + DomainException exception = assertThrows(DomainException.class, () -> authorizedMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null)); + assertThat(exception.getCode()).isEqualTo(MemberErrorCode.NOT_AUTHORIZED_MEMBER.name()); + } +} From dc5aa9837f301b8bd59b4f20bec0f1b687212ef8 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 1 Nov 2024 02:35:32 +0900 Subject: [PATCH 186/478] =?UTF-8?q?fix:=20(#67)=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20AuthorizedMember=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CreateQuickStartService.java | 8 -------- .../application/ReadQuickStartsService.java | 9 --------- .../application/UpdateQuickStartService.java | 8 -------- .../activity/exception/QuickStartErrorCode.java | 1 - .../presentation/CreateQuickStartController.java | 4 ++-- .../presentation/ReadQuickStartsController.java | 4 ++-- .../presentation/UpdateQuickStartController.java | 4 ++-- .../application/CreateQuickStartServiceTest.java | 14 -------------- 8 files changed, 6 insertions(+), 46 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java index c6446c041..156961e0c 100644 --- a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java +++ b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java @@ -20,7 +20,6 @@ public class CreateQuickStartService { public Long createQuickStart(Member member, QuickStartRequest request) { validateRequest(request); - validateMember(member); QuickStart quickStart = QuickStart.create(member.getId(), request.name(), request.startTime(), request.spareTime(), request.type()); QuickStart savedQuickStart = quickStartRepository.save(quickStart); return savedQuickStart.getId(); @@ -32,11 +31,4 @@ private void validateRequest(QuickStartRequest request) { throw QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.toException(); } } - - private void validateMember(Member member) { - if (!member.isMember()) { - log.error("[CreateQuickStartService] Client is not a member."); - throw QuickStartErrorCode.NOT_A_MEMBER.toException(); - } - } } diff --git a/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java b/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java index d06d51789..5703b4073 100644 --- a/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java +++ b/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java @@ -7,7 +7,6 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.dto.response.QuickStartResponse; import spring.backend.activity.dto.response.QuickStartsResponse; -import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.activity.query.dao.QuickStartDao; import spring.backend.member.domain.entity.Member; @@ -22,15 +21,7 @@ public class ReadQuickStartsService { private final QuickStartDao quickStartDao; public QuickStartsResponse readQuickStarts(Member member) { - validateMember(member); List quickStartResponses = quickStartDao.findByMemberId(member.getId(), Sort.by("createdAt").descending()); return new QuickStartsResponse(quickStartResponses); } - - private void validateMember(Member member) { - if (!member.isMember()) { - log.error("[ReadQuickStartsService] Client is not a member."); - throw QuickStartErrorCode.NOT_A_MEMBER.toException(); - } - } } diff --git a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java index c260eb82f..d397f5e70 100644 --- a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java +++ b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java @@ -30,7 +30,6 @@ public void updateQuickStart(Member member, QuickStartRequest request, Long quic private void validateUpdateRequest(Member member, QuickStartRequest request, QuickStart quickStart) { validateQuickStartExistence(quickStart); validateRequest(request); - validateMember(member); validateMemberId(member.getId(), quickStart.getMemberId()); } @@ -48,13 +47,6 @@ private void validateRequest(QuickStartRequest request) { } } - private void validateMember(Member member) { - if (!member.isMember()) { - log.error("[validateMember] Unauthorized member."); - throw QuickStartErrorCode.NOT_A_MEMBER.toException(); - } - } - private void validateMemberId(UUID memberId, UUID quickStartMemberId) { if (!memberId.equals(quickStartMemberId)) { log.error("[validateMemberId] Member id mismatch"); diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java index c91701918..5a58819c7 100644 --- a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java @@ -12,7 +12,6 @@ public enum QuickStartErrorCode implements BaseErrorCode { QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."), NOT_EXIST_QUICK_START_CONDITION(HttpStatus.BAD_REQUEST, "빠른 시작 요청 조건이 유효하지 않습니다."), - NOT_A_MEMBER(HttpStatus.FORBIDDEN, "사용자가 멤버 사용자가 아닙니다."), NOT_EXIST_QUICK_START(HttpStatus.BAD_REQUEST, "빠른 시작이 존재하지 않습니다."), MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "빠른 시작과 멤버 ID가 일치하지 않습니다."); diff --git a/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java b/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java index 6313b5d5d..5eb2f83aa 100644 --- a/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java +++ b/src/main/java/spring/backend/activity/presentation/CreateQuickStartController.java @@ -9,7 +9,7 @@ import spring.backend.activity.application.CreateQuickStartService; import spring.backend.activity.dto.request.QuickStartRequest; import spring.backend.activity.presentation.swagger.CreateQuickStartSwagger; -import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @@ -22,7 +22,7 @@ public class CreateQuickStartController implements CreateQuickStartSwagger { @Authorization @PostMapping("/v1/quick-starts") - public ResponseEntity> createQuickStart(@LoginMember Member member, @Valid @RequestBody QuickStartRequest request) { + public ResponseEntity> createQuickStart(@AuthorizedMember Member member, @Valid @RequestBody QuickStartRequest request) { Long memberId = createQuickStartService.createQuickStart(member, request); return ResponseEntity.ok(new RestResponse<>(memberId)); } diff --git a/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java index 9c1d1ea21..583f93d75 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadQuickStartsController.java @@ -7,7 +7,7 @@ import spring.backend.activity.application.ReadQuickStartsService; import spring.backend.activity.dto.response.QuickStartsResponse; import spring.backend.activity.presentation.swagger.ReadQuickStartsSwagger; -import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @@ -20,7 +20,7 @@ public class ReadQuickStartsController implements ReadQuickStartsSwagger { @Authorization @GetMapping("/v1/quick-starts") - public ResponseEntity> readQuickStarts(@LoginMember Member member) { + public ResponseEntity> readQuickStarts(@AuthorizedMember Member member) { return ResponseEntity.ok(new RestResponse<>(readQuickStartsService.readQuickStarts(member))); } } diff --git a/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java index bc32ddd5f..3071a0b22 100644 --- a/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java +++ b/src/main/java/spring/backend/activity/presentation/UpdateQuickStartController.java @@ -10,7 +10,7 @@ import spring.backend.activity.application.UpdateQuickStartService; import spring.backend.activity.dto.request.QuickStartRequest; import spring.backend.activity.presentation.swagger.UpdateQuickStartSwagger; -import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.member.domain.entity.Member; @@ -22,7 +22,7 @@ public class UpdateQuickStartController implements UpdateQuickStartSwagger { @Authorization @PatchMapping("/v1/quick-starts/{quickStartId}") - public ResponseEntity updateQuickStart(@LoginMember Member member, @Valid @RequestBody QuickStartRequest request, @PathVariable Long quickStartId) { + public ResponseEntity updateQuickStart(@AuthorizedMember Member member, @Valid @RequestBody QuickStartRequest request, @PathVariable Long quickStartId) { updateQuickStartService.updateQuickStart(member, request, quickStartId); return ResponseEntity.ok().build(); } diff --git a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java index 0b105efa0..f101aea80 100644 --- a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java +++ b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java @@ -34,7 +34,6 @@ class CreateQuickStartServiceTest { private QuickStartRepository quickStartRepository; private Member member; - private Member guest; private QuickStartRequest request; @BeforeEach @@ -42,9 +41,6 @@ public void setUp() { member = Member.builder() .role(Role.MEMBER) .build(); - guest = Member.builder() - .role(Role.GUEST) - .build(); request = new QuickStartRequest( "등교", LocalTime.of(12, 30), @@ -63,16 +59,6 @@ public void createQuickStart_NullRequest_ThrowsException() { assertEquals(QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.getMessage(), ex.getMessage()); } - @DisplayName("비회원이 요청하는 경우 예외가 발생한다") - @Test - public void createQuickStart_NonMember_ThrowsException() { - // when - DomainException ex = assertThrows(DomainException.class, () -> createQuickStartService.createQuickStart(guest, request)); - - // then - assertEquals(QuickStartErrorCode.NOT_A_MEMBER.getMessage(), ex.getMessage()); - } - @DisplayName("유효한 빠른 시작 요청인 경우 저장된 ID를 반환한다") @Test public void createQuickStart_ValidRequest_ReturnsSavedQuickStartId() { From c2c097b8f586d4e1cb50fbad7eb5e963c59d1905 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 1 Nov 2024 05:03:43 +0900 Subject: [PATCH 187/478] =?UTF-8?q?fix:=20(#70)=20Activity=EC=9D=98=20Keyw?= =?UTF-8?q?ord=EB=8A=94=20=ED=95=98=EB=82=98=EB=A7=8C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/activity/domain/entity/Activity.java | 3 +-- .../activity/infrastructure/mapper/ActivityMapper.java | 4 ++-- .../persistence/jpa/entity/ActivityJpaEntity.java | 7 ++----- .../activity/domain/repository/ActivityRepositoryTest.java | 5 ++--- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java index 76ff86b7a..6e325eb8b 100644 --- a/src/main/java/spring/backend/activity/domain/entity/Activity.java +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -6,7 +6,6 @@ import spring.backend.activity.domain.value.Type; import java.time.LocalDateTime; -import java.util.Set; import java.util.UUID; @Getter @@ -23,7 +22,7 @@ public class Activity { private Type type; - private Set keywords; + private Keyword keyword; private String title; diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java index 9581e4354..ff3f9941a 100644 --- a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java @@ -16,7 +16,7 @@ public Activity toDomainEntity(ActivityJpaEntity activity) { .quickStartId(activity.getQuickStartId()) .spareTime(activity.getSpareTime()) .type(activity.getType()) - .keywords(activity.getKeywords()) + .keyword(activity.getKeyword()) .title(activity.getTitle()) .content(activity.getContent()) .location(activity.getLocation()) @@ -36,7 +36,7 @@ public ActivityJpaEntity toJpaEntity(Activity activity) { .quickStartId(activity.getQuickStartId()) .spareTime(activity.getSpareTime()) .type(activity.getType()) - .keywords(activity.getKeywords()) + .keyword(activity.getKeyword()) .title(activity.getTitle()) .content(activity.getContent()) .location(activity.getLocation()) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java index 7065b4d78..dfc3ac764 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java @@ -10,7 +10,6 @@ import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; import java.time.LocalDateTime; -import java.util.Set; import java.util.UUID; @Entity @@ -29,10 +28,8 @@ public class ActivityJpaEntity extends BaseLongIdEntity { @Enumerated(EnumType.STRING) private Type type; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "activity_keyword", - joinColumns = @JoinColumn(name = "activity_id")) - private Set keywords; + @Embedded + private Keyword keyword; private String title; diff --git a/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java index be8b5ae02..03e8e9c94 100644 --- a/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java +++ b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java @@ -9,7 +9,6 @@ import spring.backend.activity.domain.value.Type; import java.time.LocalDateTime; -import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -29,7 +28,7 @@ void setUp() { .quickStartId(100L) .spareTime(120) .type(Type.ONLINE) - .keywords(Set.of(Keyword.create(Keyword.Category.SELF_DEVELOPMENT, "test.url"), Keyword.create(Keyword.Category.ENTERTAINMENT, "test1.url"))) + .keyword(Keyword.create(Keyword.Category.SELF_DEVELOPMENT, "test.url")) .title("Test Activity") .content("This is a test activity.") .location("Test Location") @@ -48,6 +47,6 @@ void testSaveAndFindActivity() { Activity foundActivity = activityRepository.findById(savedActivity.getId()); assertThat(foundActivity).isNotNull(); - assertThat(foundActivity.getKeywords()).isEqualTo(activity.getKeywords()); + assertThat(foundActivity.getKeyword()).isEqualTo(activity.getKeyword()); } } \ No newline at end of file From fc0ba53fdcf81cfa13c86a9308a72d8f93b52f73 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 2 Nov 2024 01:55:09 +0900 Subject: [PATCH 188/478] =?UTF-8?q?feat:=20(#70)=20=ED=99=88=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=91=EB=8B=B5=20DTO?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/HomeActivityInfoResponse.java | 11 +++++++++++ .../member/dto/response/HomeMainResponse.java | 17 +++++++++++++++++ .../dto/response/HomeMemberInfoResponse.java | 15 +++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java create mode 100644 src/main/java/spring/backend/member/dto/response/HomeMainResponse.java create mode 100644 src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java diff --git a/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java new file mode 100644 index 000000000..9318cea90 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java @@ -0,0 +1,11 @@ +package spring.backend.activity.dto.response; + +import spring.backend.activity.domain.value.Keyword; + +public record HomeActivityInfoResponse( + Long id, + Keyword keyword, + String title, + Integer savedTime +) { +} diff --git a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java new file mode 100644 index 000000000..9d780ebb1 --- /dev/null +++ b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java @@ -0,0 +1,17 @@ +package spring.backend.member.dto.response; + +import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.activity.dto.response.QuickStartResponse; + +import java.util.List; + +public record HomeMainResponse( + HomeMemberInfoResponse member, + QuickStartResponse quickStart, + int totalSavedTime, + List activities +) { + public static HomeMainResponse of(HomeMemberInfoResponse member, QuickStartResponse quickStart, int totalSavedTime, List activities) { + return new HomeMainResponse(member, quickStart, totalSavedTime, activities); + } +} diff --git a/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java b/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java new file mode 100644 index 000000000..022bfe008 --- /dev/null +++ b/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java @@ -0,0 +1,15 @@ +package spring.backend.member.dto.response; + +import spring.backend.member.domain.entity.Member; + +import java.util.UUID; + +public record HomeMemberInfoResponse( + UUID id, + String nickname, + String profileImage +) { + public static HomeMemberInfoResponse from(Member member) { + return new HomeMemberInfoResponse(member.getId(), member.getNickname(), member.getProfileImage()); + } +} From b53608bc1589209187f39aaa8065024f63a41aaf Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 2 Nov 2024 01:56:16 +0900 Subject: [PATCH 189/478] =?UTF-8?q?feat:=20(#70)=20=EB=8B=B9=EC=9D=BC=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=EB=90=9C=20=ED=99=9C=EB=8F=99=EC=9D=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/ActivityJpaDao.java | 30 +++++++++++++++++++ .../activity/query/dao/ActivityDao.java | 12 ++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java create mode 100644 src/main/java/spring/backend/activity/query/dao/ActivityDao.java diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java new file mode 100644 index 000000000..365d2a467 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -0,0 +1,30 @@ +package spring.backend.activity.infrastructure.persistence.jpa.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; +import spring.backend.activity.query.dao.ActivityDao; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public interface ActivityJpaDao extends JpaRepository, ActivityDao { + + @Override + @Query(""" + select new spring.backend.activity.dto.response.HomeActivityInfoResponse( + a.id, + a.keyword, + a.title, + a.savedTime + ) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + order by a.createdAt ASC + """) + List findTodayActivities(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); +} \ No newline at end of file diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java new file mode 100644 index 000000000..be1e2bb39 --- /dev/null +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -0,0 +1,12 @@ +package spring.backend.activity.query.dao; + +import spring.backend.activity.dto.response.HomeActivityInfoResponse; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public interface ActivityDao { + + List findTodayActivities(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); +} From 6d71938a572f9e5b666f898d19e135a59698efd7 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 2 Nov 2024 01:57:31 +0900 Subject: [PATCH 190/478] =?UTF-8?q?feat:=20(#70)=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=B4=20=EA=B0=80=EC=9E=A5=20=EA=B7=BC?= =?UTF-8?q?=EC=A0=91=ED=95=9C=20=EB=B9=A0=EB=A5=B8=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/QuickStartJpaDao.java | 16 ++++++++++++++++ .../activity/query/dao/QuickStartDao.java | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java index 834793047..b481a6be4 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java @@ -25,4 +25,20 @@ public interface QuickStartJpaDao extends JpaRepository findByMemberId(UUID memberId, Sort sort); + + @Override + @Query(""" + select new spring.backend.activity.dto.response.QuickStartResponse( + q.id, + q.name, + q.startTime, + q.spareTime, + q.type + ) + from QuickStartJpaEntity q + where q.memberId = :memberId + and q.startTime > CURRENT_TIMESTAMP + order by q.startTime ASC + """) + List findUpcomingQuickStarts(UUID memberId); } \ No newline at end of file diff --git a/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java b/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java index 4183651c8..eb9176c80 100644 --- a/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java +++ b/src/main/java/spring/backend/activity/query/dao/QuickStartDao.java @@ -9,4 +9,6 @@ public interface QuickStartDao { List findByMemberId(UUID memberId, Sort sort); + + List findUpcomingQuickStarts(UUID memberId); } From 079e69defc636f1842b8ef3947f60b41de683e4a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 2 Nov 2024 02:06:52 +0900 Subject: [PATCH 191/478] =?UTF-8?q?feat:=20(#70)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=ED=99=88=20=ED=99=94=EB=A9=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadMemberHomeService.java | 56 +++++++++++++++++++ .../ReadMemberHomeController.java | 25 +++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/main/java/spring/backend/member/application/ReadMemberHomeService.java create mode 100644 src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java diff --git a/src/main/java/spring/backend/member/application/ReadMemberHomeService.java b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java new file mode 100644 index 000000000..295d6899d --- /dev/null +++ b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java @@ -0,0 +1,56 @@ +package spring.backend.member.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.activity.dto.response.QuickStartResponse; +import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.activity.query.dao.QuickStartDao; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.dto.response.HomeMemberInfoResponse; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional(readOnly = true) +public class ReadMemberHomeService { + + private final ActivityDao activityDao; + + private final QuickStartDao quickStartDao; + + public HomeMainResponse readMemberHome(Member member) { + HomeMemberInfoResponse memberInfo = HomeMemberInfoResponse.from(member); + + List upcomingQuickStarts = quickStartDao.findUpcomingQuickStarts(member.getId()); + QuickStartResponse upcomingQuickStart = upcomingQuickStarts.stream().findFirst().orElse(null); + + LocalDateTime currentDateTime = LocalDateTime.now(); + List activities = activityDao.findTodayActivities(member.getId(), currentDateTime.toLocalDate().atStartOfDay(), currentDateTime); + int totalSavedTime = calculateTotalSavedTime(activities); + + return HomeMainResponse.of(memberInfo, upcomingQuickStart, totalSavedTime, activities); + } + + private int calculateTotalSavedTime(List activities) { + if (activities == null || activities.isEmpty()) { + log.info("[ReadMemberHomeService] activities is empty"); + return 0; + } + return activities.stream() + .mapToInt(activity -> { + if (activity == null) { + log.info("[ReadMemberHomeService] activity is null"); + return 0; + } + return activity.savedTime(); + }) + .sum(); + } +} diff --git a/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java new file mode 100644 index 000000000..dc4542dfd --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java @@ -0,0 +1,25 @@ +package spring.backend.member.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.application.ReadMemberHomeService; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.HomeMainResponse; + +@RestController +@RequiredArgsConstructor +public class ReadMemberHomeController { + + private final ReadMemberHomeService readMemberHomeService; + + @Authorization + @GetMapping("/v1/home") + public ResponseEntity> readMemberHome(@AuthorizedMember Member member) { + return ResponseEntity.ok(new RestResponse<>(readMemberHomeService.readMemberHome(member))); + } +} From e2ebe733b5d9fe99b295d1b4ec1e8801065701af Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 2 Nov 2024 02:34:17 +0900 Subject: [PATCH 192/478] =?UTF-8?q?feat:=20(#70)=20=ED=99=88=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=8A=A4=EC=9B=A8?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/HomeActivityInfoResponse.java | 9 +++++++ .../member/dto/response/HomeMainResponse.java | 9 +++++++ .../dto/response/HomeMemberInfoResponse.java | 7 ++++++ .../ReadMemberHomeController.java | 3 ++- .../swagger/ReadMemberHomeSwagger.java | 24 +++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java diff --git a/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java index 9318cea90..4c37a67d1 100644 --- a/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java @@ -1,11 +1,20 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword; public record HomeActivityInfoResponse( + + @Schema(description = "활동 ID", example = "1") Long id, + + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") Keyword keyword, + + @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") String title, + + @Schema(description = "모은 시간", example = "60") Integer savedTime ) { } diff --git a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java index 9d780ebb1..da4ed6779 100644 --- a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java +++ b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java @@ -1,14 +1,23 @@ package spring.backend.member.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.dto.response.HomeActivityInfoResponse; import spring.backend.activity.dto.response.QuickStartResponse; import java.util.List; public record HomeMainResponse( + + @Schema(description = "회원 정보") HomeMemberInfoResponse member, + + @Schema(description = "빠른 시작") QuickStartResponse quickStart, + + @Schema(description = "총 모은 시간", example = "120") int totalSavedTime, + + @Schema(description = "활동 목록") List activities ) { public static HomeMainResponse of(HomeMemberInfoResponse member, QuickStartResponse quickStart, int totalSavedTime, List activities) { diff --git a/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java b/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java index 022bfe008..8f5ddfd6c 100644 --- a/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java +++ b/src/main/java/spring/backend/member/dto/response/HomeMemberInfoResponse.java @@ -1,12 +1,19 @@ package spring.backend.member.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.member.domain.entity.Member; import java.util.UUID; public record HomeMemberInfoResponse( + + @Schema(description = "멤버 ID", example = "4d45be4b-1cb0-4760-b826-7afc505783cd") UUID id, + + @Schema(description = "닉네임", example = "조각조각") String nickname, + + @Schema(description = "프로필 이미지 URL", example = "https://example.com/image.jpg") String profileImage ) { public static HomeMemberInfoResponse from(Member member) { diff --git a/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java index dc4542dfd..4594f0982 100644 --- a/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java +++ b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java @@ -10,10 +10,11 @@ import spring.backend.member.application.ReadMemberHomeService; import spring.backend.member.domain.entity.Member; import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.presentation.swagger.ReadMemberHomeSwagger; @RestController @RequiredArgsConstructor -public class ReadMemberHomeController { +public class ReadMemberHomeController implements ReadMemberHomeSwagger { private final ReadMemberHomeService readMemberHomeService; diff --git a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java new file mode 100644 index 000000000..50889512b --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java @@ -0,0 +1,24 @@ +package spring.backend.member.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.exception.MemberErrorCode; + +@Tag(name = "Member", description = "멤버") +public interface ReadMemberHomeSwagger { + + @Operation( + summary = "홈 메인페이지 조회 API", + description = "사용자의 가장 근접한 빠른시작과 당일 모은 활동내역을 보여줍니다.", + operationId = "/v1/home" + ) + @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) + ResponseEntity> readMemberHome(@Parameter(hidden = true) Member member); +} From deacd5ca0ee1b3201e11ccea9301a4b780974b21 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 3 Nov 2024 19:57:11 +0900 Subject: [PATCH 193/478] =?UTF-8?q?feat:=20(#73)=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=9C=A0=ED=8B=B8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/CreateQuickStartService.java | 16 +++++- .../application/UpdateQuickStartService.java | 15 +++++- .../exception/QuickStartErrorCode.java | 3 +- .../spring/backend/core/util/TimeUtil.java | 52 +++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/main/java/spring/backend/core/util/TimeUtil.java diff --git a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java index 156961e0c..e0ecb9f27 100644 --- a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java +++ b/src/main/java/spring/backend/activity/application/CreateQuickStartService.java @@ -8,8 +8,11 @@ import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.activity.dto.request.QuickStartRequest; import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; +import java.time.LocalTime; + @Service @RequiredArgsConstructor @Log4j2 @@ -20,7 +23,11 @@ public class CreateQuickStartService { public Long createQuickStart(Member member, QuickStartRequest request) { validateRequest(request); - QuickStart quickStart = QuickStart.create(member.getId(), request.name(), request.startTime(), request.spareTime(), request.type()); + + LocalTime startTime = TimeUtil.toLocalTime(request.meridiem(), request.hour(), request.minute()); + validateStartTime(startTime); + + QuickStart quickStart = QuickStart.create(member.getId(), request.name(), startTime, request.spareTime(), request.type()); QuickStart savedQuickStart = quickStartRepository.save(quickStart); return savedQuickStart.getId(); } @@ -31,4 +38,11 @@ private void validateRequest(QuickStartRequest request) { throw QuickStartErrorCode.NOT_EXIST_QUICK_START_CONDITION.toException(); } } + + private void validateStartTime(LocalTime time) { + if (time == null) { + log.error("[CreateQuickStartService] Invalid start time."); + throw QuickStartErrorCode.START_TIME_CONVERSION_FAILED.toException(); + } + } } diff --git a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java index d397f5e70..ec8d1ac49 100644 --- a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java +++ b/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java @@ -8,8 +8,10 @@ import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.activity.dto.request.QuickStartRequest; import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; +import java.time.LocalTime; import java.util.UUID; @Service @@ -23,7 +25,11 @@ public class UpdateQuickStartService { public void updateQuickStart(Member member, QuickStartRequest request, Long quickStartId) { QuickStart quickStart = quickStartRepository.findById(quickStartId); validateUpdateRequest(member, request, quickStart); - quickStart.update(request.name(), request.startTime(), request.spareTime(), request.type()); + + LocalTime startTime = TimeUtil.toLocalTime(request.meridiem(), request.hour(), request.minute()); + validateStartTime(startTime); + + quickStart.update(request.name(), startTime, request.spareTime(), request.type()); quickStartRepository.save(quickStart); } @@ -53,4 +59,11 @@ private void validateMemberId(UUID memberId, UUID quickStartMemberId) { throw QuickStartErrorCode.MEMBER_ID_MISMATCH.toException(); } } + + private void validateStartTime(LocalTime time) { + if (time == null) { + log.error("[UpdateQuickStartService] Invalid start time."); + throw QuickStartErrorCode.START_TIME_CONVERSION_FAILED.toException(); + } + } } diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java index 5a58819c7..1969c85e6 100644 --- a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java @@ -13,7 +13,8 @@ public enum QuickStartErrorCode implements BaseErrorCode { QUICK_START_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "빠른 시작 정보를 저장하는데 실패하였습니다."), NOT_EXIST_QUICK_START_CONDITION(HttpStatus.BAD_REQUEST, "빠른 시작 요청 조건이 유효하지 않습니다."), NOT_EXIST_QUICK_START(HttpStatus.BAD_REQUEST, "빠른 시작이 존재하지 않습니다."), - MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "빠른 시작과 멤버 ID가 일치하지 않습니다."); + MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "빠른 시작과 멤버 ID가 일치하지 않습니다."), + START_TIME_CONVERSION_FAILED(HttpStatus.BAD_REQUEST, "빠른 시작 시작 시간 변환에 실패하였습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/core/util/TimeUtil.java b/src/main/java/spring/backend/core/util/TimeUtil.java new file mode 100644 index 000000000..a29749cca --- /dev/null +++ b/src/main/java/spring/backend/core/util/TimeUtil.java @@ -0,0 +1,52 @@ +package spring.backend.core.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.DateTimeException; +import java.time.LocalTime; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TimeUtil { + + private static final String ANTE_MERIDIEM = "오전"; + + private static final String POST_MERIDIEM = "오후"; + + public static LocalTime toLocalTime(String meridiem, Integer hour, Integer minute) { + if (meridiem == null || hour == null || minute == null) { + return null; + } + try { + return switch (meridiem) { + case ANTE_MERIDIEM -> LocalTime.of(hour % 12, minute); + case POST_MERIDIEM -> LocalTime.of((hour % 12) + 12, minute); + default -> null; + }; + } catch (DateTimeException e) { + return null; + } + } + + public static String toMeridiem(LocalTime time) { + if (time == null) { + return null; + } + return time.getHour() < 12 ? ANTE_MERIDIEM : POST_MERIDIEM; + } + + public static Integer toHour(LocalTime time) { + if (time == null) { + return null; + } + int hour = time.getHour() % 12; + return hour == 0 ? 12 : hour; + } + + public static Integer toMinute(LocalTime time) { + if (time == null) { + return null; + } + return time.getMinute(); + } +} From 1752bfbc5ced564108205e6b15c6d6fb038fb2e8 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 3 Nov 2024 20:00:13 +0900 Subject: [PATCH 194/478] =?UTF-8?q?fix:=20(#73)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=9A=94=EC=B2=AD,=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=97=90=20=EC=9E=90=EC=84=B8=ED=95=9C=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EA=B0=80=20=EC=B6=94=EA=B0=80=EB=90=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/QuickStartRequest.java | 21 ++++++++++++++----- .../dto/response/QuickStartResponse.java | 15 ++++++++++++- .../CreateQuickStartServiceTest.java | 8 +++++-- .../dto/request/QuickStartRequestTest.java | 9 ++++---- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java index d79afb320..5100835e5 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.*; import spring.backend.activity.domain.value.Type; -import java.time.LocalTime; - public record QuickStartRequest( @NotNull(message = "이름은 필수 입력 항목입니다.") @@ -14,9 +12,22 @@ public record QuickStartRequest( @Schema(description = "빠른 시작 이름", example = "등교") String name, - @NotNull(message = "시작 시간은 필수 입력 항목입니다.") - @Schema(description = "시작 시간", example = "12:30") - LocalTime startTime, + @NotNull(message = "시작 시간의 시간은 필수 입력 항목입니다.") + @Min(value = 0, message = "시작 시간의 시간은 최소 0이어야 합니다.") + @Max(value = 12, message = "시작 시간의 시간은 최대 12이어야 합니다.") + @Schema(description = "시작 시간의 시간", example = "12") + Integer hour, + + @NotNull(message = "시작 시간의 분은 필수 입력 항목입니다.") + @Min(value = 0, message = "시작 시간의 분은 최소 0이어야 합니다.") + @Max(value = 59, message = "시작 시간의 분은 최대 59이어야 합니다.") + @Schema(description = "시작 시간의 분", example = "30") + Integer minute, + + @NotNull(message = "오전/오후 표시는 필수 입력 항목입니다.") + @Pattern(regexp = "^(오전|오후)$", message = "meridiem은 '오전' 또는 '오후'여야 합니다.") + @Schema(description = "오전/오후 표시", example = "오후") + String meridiem, @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java index d33147a99..8d34f4441 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Type; +import spring.backend.core.util.TimeUtil; import java.time.LocalTime; @@ -20,6 +21,18 @@ public record QuickStartResponse( Integer spareTime, @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") - Type type + Type type, + + @Schema(description = "시작 시간의 시", example = "12") + Integer hour, + + @Schema(description = "시작 시간의 분", example = "30") + Integer minute, + + @Schema(description = "오전/오후 표시", example = "오후") + String meridiem ) { + public QuickStartResponse(Long id, String name, LocalTime startTime, Integer spareTime, Type type) { + this(id, name, startTime, spareTime, type, TimeUtil.toHour(startTime), TimeUtil.toMinute(startTime), TimeUtil.toMeridiem(startTime)); + } } diff --git a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java index f101aea80..06b11090a 100644 --- a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java +++ b/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java @@ -13,6 +13,7 @@ import spring.backend.activity.dto.request.QuickStartRequest; import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.core.exception.DomainException; +import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; @@ -43,7 +44,9 @@ public void setUp() { .build(); request = new QuickStartRequest( "등교", - LocalTime.of(12, 30), + 12, + 30, + "오전", 300, Type.ONLINE ); @@ -62,7 +65,8 @@ public void createQuickStart_NullRequest_ThrowsException() { @DisplayName("유효한 빠른 시작 요청인 경우 저장된 ID를 반환한다") @Test public void createQuickStart_ValidRequest_ReturnsSavedQuickStartId() { - QuickStart quickStart = QuickStart.create(member.getId(), request.name(), request.startTime(), request.spareTime(), request.type()); + LocalTime startTime = TimeUtil.toLocalTime(request.meridiem(), request.hour(), request.minute()); + QuickStart quickStart = QuickStart.create(member.getId(), request.name(), startTime, request.spareTime(), request.type()); when(quickStartRepository.save(any(QuickStart.class))).thenReturn(quickStart); // when diff --git a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java index 4b42f36f8..66f6a19eb 100644 --- a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java +++ b/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.params.provider.ValueSource; import spring.backend.activity.domain.value.Type; -import java.time.LocalTime; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +32,7 @@ class NameValidationTests { @Test @DisplayName("null 값일 경우 에러가 발생한다.") void whenNameIsNull_thenValidationFails() { - QuickStartRequest request = new QuickStartRequest(null, LocalTime.now(), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(null, 12, 30, "오전", 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); @@ -44,7 +43,7 @@ void whenNameIsNull_thenValidationFails() { @DisplayName("올바른 형식의 이름일 경우 성공한다.") @ValueSource(strings = {"등교", "이름테스트", "John Doe", "사용자1"}) void whenNameIsValid_thenValidationSucceeds(String name) { - QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, 12, 30, "오전", 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isEmpty(); @@ -54,7 +53,7 @@ void whenNameIsValid_thenValidationSucceeds(String name) { @DisplayName("형식에 맞지 않는 이름일 경우 에러가 발생한다.") @ValueSource(strings = {" 이름", "이름 ", "이름@이름", "공백 공백"}) void whenNameIsInvalid_thenValidationFails(String name) { - QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, 12, 30, "오전", 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); @@ -65,7 +64,7 @@ void whenNameIsInvalid_thenValidationFails(String name) { @DisplayName("10자를 초과하는 경우 에러가 발생한다.") void whenNameExceedsMaxLength_thenValidationFails() { String name = "매우몹시너무긴이름longname"; - QuickStartRequest request = new QuickStartRequest(name, LocalTime.now(), 300, Type.OFFLINE); + QuickStartRequest request = new QuickStartRequest(name, 12, 30, "오전", 300, Type.OFFLINE); Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); From 5cbf7fe447daafea31e8d5613bd51e775c073f64 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 3 Nov 2024 20:11:24 +0900 Subject: [PATCH 195/478] =?UTF-8?q?feat:=20(#73)=20TimeUtil=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/util/TimeUtilTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/java/spring/backend/core/util/TimeUtilTest.java diff --git a/src/test/java/spring/backend/core/util/TimeUtilTest.java b/src/test/java/spring/backend/core/util/TimeUtilTest.java new file mode 100644 index 000000000..a79521887 --- /dev/null +++ b/src/test/java/spring/backend/core/util/TimeUtilTest.java @@ -0,0 +1,53 @@ +package spring.backend.core.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TimeUtilTest { + + @DisplayName("오전 10시 30분을 입력하면 10시 30분으로 변환된다.") + @Test + void testToLocalTime_AM() { + LocalTime time = TimeUtil.toLocalTime("오전", 10, 30); + assertEquals(10, time.getHour()); + assertEquals(30, time.getMinute()); + } + + @DisplayName("오후 10시 30분을 입력하면 22시 30분으로 변환된다.") + @Test + void testToLocalTime_PM() { + LocalTime time = TimeUtil.toLocalTime("오후", 10, 30); + assertEquals(22, time.getHour()); + assertEquals(30, time.getMinute()); + } + + @DisplayName("오전 0시 입력 시 '오전' 반환") + @Test + void testToMeridiem_Midnight() { + LocalTime time = LocalTime.of(0, 0); + String meridiem = TimeUtil.toMeridiem(time); + assertEquals("오전", meridiem); + } + + @DisplayName("오후 12시 입력 시 '오후' 반환") + @Test + void testToMeridiem_Noon() { + LocalTime time = LocalTime.of(12, 0); + String meridiem = TimeUtil.toMeridiem(time); + assertEquals("오후", meridiem); + } + + @DisplayName("오전 0시, 오후 12시는 12로 변환된다.") + @Test + void testToHour() { + LocalTime time = LocalTime.of(0, 0); + assertEquals(12, TimeUtil.toHour(time)); + + time = LocalTime.of(12, 0); + assertEquals(12, TimeUtil.toHour(time)); + } +} \ No newline at end of file From bc977130e996160a55548232e2575106f16fe6bf Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 4 Nov 2024 17:57:49 +0900 Subject: [PATCH 196/478] =?UTF-8?q?feat:=20(#77)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/entity/Activity.java | 31 +++++++++++++++++++ .../activity/exception/ActivityErrorCode.java | 6 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java index 6e325eb8b..4f65712e4 100644 --- a/src/main/java/spring/backend/activity/domain/entity/Activity.java +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -4,14 +4,18 @@ import lombok.Getter; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; +import spring.backend.activity.exception.ActivityErrorCode; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.UUID; @Getter @Builder public class Activity { + public static final Integer MAX_SAVED_TIME = 300; + private Long id; private UUID memberId; @@ -41,4 +45,31 @@ public class Activity { private LocalDateTime updatedAt; private Boolean deleted; + + public void validateActivityOwner(UUID memberId) { + if (!this.memberId.equals(memberId)) { + throw ActivityErrorCode.MEMBER_ID_MISMATCH.toException(); + } + } + + public boolean isFinished() { + return finished != null && finished; + } + + public void finish() { + if (isFinished()) { + throw ActivityErrorCode.ALREADY_FINISHED_ACTIVITY.toException(); + } + finished = true; + finishedAt = LocalDateTime.now(); + savedTime = calculateSavedTime(); + } + + private Integer calculateSavedTime() { + long savedTime = ChronoUnit.MINUTES.between(createdAt, finishedAt); + if (savedTime < 0 || savedTime > MAX_SAVED_TIME) { + throw ActivityErrorCode.INVALID_ACTIVITY_DURATION.toException(); + } + return Math.toIntExact(savedTime); + } } diff --git a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java index 272b76fe5..ea9aaf2e5 100644 --- a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java @@ -10,7 +10,11 @@ @RequiredArgsConstructor public enum ActivityErrorCode implements BaseErrorCode { - ACTIVITY_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "활동을 저장하는데 실패하였습니다."); + ACTIVITY_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "활동을 저장하는데 실패하였습니다."), + NOT_EXIST_ACTIVITY(HttpStatus.BAD_REQUEST, "활동이 존재하지 않습니다."), + MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "활동과 멤버 ID가 일치하지 않습니다."), + INVALID_ACTIVITY_DURATION(HttpStatus.BAD_REQUEST, "활동 지속 시간이 허용된 범위를 초과했습니다."), + ALREADY_FINISHED_ACTIVITY(HttpStatus.BAD_REQUEST, "이미 종료된 활동입니다."); private final HttpStatus httpStatus; From 245dd26e3beb4a8126bea8604cb67042d3f74afc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 4 Nov 2024 18:10:16 +0900 Subject: [PATCH 197/478] =?UTF-8?q?feat:=20(#77)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=ED=99=9C=EB=8F=99=EC=9D=84=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/FinishActivityService.java | 44 +++++++++++++++++++ .../activity/dto/response/ActivityInfo.java | 27 ++++++++++++ .../dto/response/FinishActivityResponse.java | 14 ++++++ .../FinishActivityController.java | 26 +++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/FinishActivityService.java create mode 100644 src/main/java/spring/backend/activity/dto/response/ActivityInfo.java create mode 100644 src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java create mode 100644 src/main/java/spring/backend/activity/presentation/FinishActivityController.java diff --git a/src/main/java/spring/backend/activity/application/FinishActivityService.java b/src/main/java/spring/backend/activity/application/FinishActivityService.java new file mode 100644 index 000000000..c8d729f05 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/FinishActivityService.java @@ -0,0 +1,44 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.dto.response.ActivityInfo; +import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.HomeMemberInfoResponse; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional +public class FinishActivityService { + + private final ActivityRepository activityRepository; + + public FinishActivityResponse finishActivity(Member member, Long activityId) { + Activity activity = activityRepository.findById(activityId); + validateActivity(activity, member); + activity.finish(); + activityRepository.save(activity); + return toResponse(member, activity); + } + + private void validateActivity(Activity activity, Member member) { + if (activity == null) { + log.error("[FinishActivityService.validateActivity] activity is null"); + throw ActivityErrorCode.NOT_EXIST_ACTIVITY.toException(); + } + activity.validateActivityOwner(member.getId()); + } + + private FinishActivityResponse toResponse(Member member, Activity activity) { + HomeMemberInfoResponse memberInfo = HomeMemberInfoResponse.from(member); + ActivityInfo activityInfo = ActivityInfo.from(activity); + return new FinishActivityResponse(memberInfo, activityInfo); + } +} diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityInfo.java b/src/main/java/spring/backend/activity/dto/response/ActivityInfo.java new file mode 100644 index 000000000..38e7eb586 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/ActivityInfo.java @@ -0,0 +1,27 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.value.Keyword; + +public record ActivityInfo( + + @Schema(description = "활동 ID", example = "1") + Long id, + + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") + Keyword keyword, + + @Schema(description = "활동 제목", example = "휴식에는 역시 명상이 최고!") + String title, + + @Schema(description = "활동 내용", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") + String content, + + @Schema(description = "모은 시간", example = "20") + Integer savedTime +) { + public static ActivityInfo from(Activity activity) { + return new ActivityInfo(activity.getId(), activity.getKeyword(), activity.getTitle(), activity.getContent(), activity.getSavedTime()); + } +} diff --git a/src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java b/src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java new file mode 100644 index 000000000..d1f3e4d0d --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java @@ -0,0 +1,14 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.member.dto.response.HomeMemberInfoResponse; + +public record FinishActivityResponse( + + @Schema(description = "회원 정보") + HomeMemberInfoResponse member, + + @Schema(description = "활동 정보") + ActivityInfo activity +) { +} diff --git a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java new file mode 100644 index 000000000..7e762b65d --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java @@ -0,0 +1,26 @@ +package spring.backend.activity.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.FinishActivityService; +import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class FinishActivityController { + + private final FinishActivityService finishActivityService; + + @Authorization + @PatchMapping("/v1/activities/{activityId}/end") + public ResponseEntity> finishActivity(@AuthorizedMember Member member, @PathVariable Long activityId) { + return ResponseEntity.ok(new RestResponse<>(finishActivityService.finishActivity(member, activityId))); + } +} From 0664b37ee5855b9370a0a452ec5403424dac8640 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 4 Nov 2024 18:17:31 +0900 Subject: [PATCH 198/478] =?UTF-8?q?feat:=20(#77)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20API=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinishActivityController.java | 5 ++-- .../swagger/FinishActivitySwagger.java | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java index 7e762b65d..7e9e71efb 100644 --- a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java +++ b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.FinishActivityService; import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.presentation.swagger.FinishActivitySwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; @@ -14,12 +15,12 @@ @RestController @RequiredArgsConstructor -public class FinishActivityController { +public class FinishActivityController implements FinishActivitySwagger { private final FinishActivityService finishActivityService; @Authorization - @PatchMapping("/v1/activities/{activityId}/end") + @PatchMapping("/v1/activities/{activityId}/finish") public ResponseEntity> finishActivity(@AuthorizedMember Member member, @PathVariable Long activityId) { return ResponseEntity.ok(new RestResponse<>(finishActivityService.finishActivity(member, activityId))); } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java new file mode 100644 index 000000000..b345d519a --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java @@ -0,0 +1,24 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "Activity", description = "활동") +public interface FinishActivitySwagger { + + @Operation( + summary = "활동 종료 API", + description = "진행 중인 활동을 종료합니다. \n\n 이 API는 활동이 자투리 시간 내에서만 종료될 수 있습니다.", + operationId = "/v1/activities/{activityId}/finish" + ) + @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) + ResponseEntity> finishActivity(@Parameter(hidden = true) Member member, Long activityId); +} From 2c66aac994d91c77ecb9ddef84e5ddfa0590db4f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:10:42 +0900 Subject: [PATCH 199/478] =?UTF-8?q?feat:=20(#64)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EB=B0=9B=EC=9D=84=20?= =?UTF-8?q?dto=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/UserActivitySelectRequest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java diff --git a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java new file mode 100644 index 000000000..362fc31ca --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java @@ -0,0 +1,39 @@ +package spring.backend.activity.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; + +import java.util.Set; + +public record UserActivitySelectRequest( + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") + Type type, + + @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") + @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") + @Max(value = 300, message = "자투리 시간은 최대 300이어야 합니다.") + @Schema(description = "자투리 시간", example = "300") + Integer spareTime, + + @NotNull(message = "키워드는 최대 5가지 입력 가능하며, 키워드를 선택하지 않은 경우 빈 배열을 보내주세요") + @Schema(description = "키워드(SELF_DEVELOPMENT, HEALTH, NATURE, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", + example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}, {\"category\": \"HEALTH\", \"image\": \"image_url\"}]") + Set keywords, + + @NotNull(message = "타이틀은 필수 입력 항목입니다.") + @Schema(description = "타이틀", example = "카페에서 커피 마시며 책 읽기") + String title, + + @NotNull(message = "Content는 필수 입력 항목입니다.") + @Schema(description = "내용", example = "조용한 카페에서 좋아하는 책을 읽으며 여유로운 시간을 즐길 수 있습니다.") + String content, + + @Schema(description = "장소", example = "서울시 강남구 역삼동") + String location +) { +} From eb887cca6bfe23ce8b6e35d66107ef656ed54500 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:12:34 +0900 Subject: [PATCH 200/478] =?UTF-8?q?feat:=20(#64)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=99=9C=EB=8F=99=20=EC=84=A0=ED=83=9D=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserActivitySelectController.java | 30 +++++++++++++++++++ .../swagger/UserActivitySelectSwagger.java | 24 +++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java new file mode 100644 index 000000000..262ea3221 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java @@ -0,0 +1,30 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.UserActivitySelectService; +import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.presentation.swagger.UserActivitySelectSwagger; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class UserActivitySelectController implements UserActivitySelectSwagger { + + private final UserActivitySelectService userActivitySelectService; + + @Authorization + @PostMapping("/v1/user-activity-selection") + @Override + public ResponseEntity> userActivitySelection(@LoginMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { + Long savedActivityId = userActivitySelectService.userActivitySelection(member, userActivitySelectRequest); + return ResponseEntity.ok(new RestResponse<>(savedActivityId)); + } +} diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java new file mode 100644 index 000000000..1d931614f --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java @@ -0,0 +1,24 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "UserActivitySelection", description = "활동 선택") +public interface UserActivitySelectSwagger { + + @Operation( + summary = "사용자 활동 선택 API", + description = "사용자가 추천받은 활동 중 한가지 활동을 선택합니다.", + operationId = "/v1/user-activity-selection" + ) + @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) + ResponseEntity> userActivitySelection(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); +} From 522223144f625071175782da550a2cbf0c498818 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:13:11 +0900 Subject: [PATCH 201/478] =?UTF-8?q?feat:=20(#64)=20=ED=99=9C=EB=8F=99?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EB=A5=BC=20=EB=A7=8C=EB=93=A0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/domain/entity/Activity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java index 4f65712e4..b0766c657 100644 --- a/src/main/java/spring/backend/activity/domain/entity/Activity.java +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -72,4 +72,17 @@ private Integer calculateSavedTime() { } return Math.toIntExact(savedTime); } + + public static Activity create(UUID memberId, Integer spareTime, Type type, Keyword keyword, String title, String content, String location) { + return Activity.builder() + .memberId(memberId) + .spareTime(spareTime) + .type(type) + .keyword(keyword) + .title(title) + .content(content) + .location(location) + .finished(false) + .build(); + } } From 496850b8431ee3a2eb01997eb632e5994f5c40ce Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:13:26 +0900 Subject: [PATCH 202/478] =?UTF-8?q?feat:=20(#64)=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=84=A0=ED=83=9D=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EB=A7=8C=EB=93=A0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserActivitySelectService.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/UserActivitySelectService.java diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java new file mode 100644 index 000000000..5867c0708 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -0,0 +1,34 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.member.domain.entity.Member; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional +public class UserActivitySelectService { + + private final ActivityRepository activityRepository; + + public Long userActivitySelection(Member member, UserActivitySelectRequest userActivitySelectRequest) { + validateRequest(userActivitySelectRequest); + Activity activity = Activity.create(member.getId(), userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity savedActivity = activityRepository.save(activity); + return savedActivity.getId(); + } + + private void validateRequest(UserActivitySelectRequest userActivitySelectRequest) { + if (userActivitySelectRequest == null) { + log.error("[UserActivitySelectRequest] Invalid request."); + throw ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.toException(); + } + } +} From 1c49ad5c720da128041c659640b2fd758d4cfa90 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:13:37 +0900 Subject: [PATCH 203/478] =?UTF-8?q?feat:=20(#64)=20UserActivitySelectServi?= =?UTF-8?q?ceTest=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserActivitySelectServiceTest.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java new file mode 100644 index 000000000..b3f05bbba --- /dev/null +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -0,0 +1,87 @@ +package spring.backend.activity.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; +import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Role; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UserActivitySelectServiceTest { + @InjectMocks + private UserActivitySelectService userActivitySelectService; + + @Mock + private ActivityRepository activityRepository; + + private Member member; + private Member guest; + private UserActivitySelectRequest userActivitySelectRequest; + + @BeforeEach + public void setUp() { + member = Member.builder() + .role(Role.MEMBER) + .build(); + + Set keywords = Set.of( + Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png"), + Keyword.create(Keyword.Category.HEALTH, "example-image2.png") + ); + + + userActivitySelectRequest = new UserActivitySelectRequest( + Type.OFFLINE, + 150, + keywords, + "title", + "content", + "location" + ); + } + + @DisplayName("요청이 null인 경우 예외가 발생한다") + @Test + public void throwsExceptionWhenUserActivitySelectRequestIsNull() { + // when + DomainException ex = assertThrows(DomainException.class, () -> userActivitySelectService.userActivitySelection(member, null)); + + // then + assertEquals(ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.getMessage(), ex.getMessage()); + } + + @DisplayName("유효한 활동 선택인 경우 저장된 ID를 반환한다") + @Test + public void returnsSavedActivityIdWhenValidActivitySelection() { + // when + Activity activity = Activity.create(member.getId(), userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + when(activityRepository.save(any(Activity.class))).thenReturn(activity); + + // then + Long savedActivityId = userActivitySelectService.userActivitySelection(member, userActivitySelectRequest); + + // then + assertEquals(activity.getId(), savedActivityId); + verify(activityRepository).save(any(Activity.class)); + } + +} From bee4a8d8f2df23a1510c17cc5ee20f9c726616b2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 11:13:54 +0900 Subject: [PATCH 204/478] =?UTF-8?q?feat:=20(#64)=20GetRecommendationsFromC?= =?UTF-8?q?lovaController=EC=97=90=20Authorziation=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/GetRecommendationsFromClovaController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java index b7055849c..3f7c210c2 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.recommendation.application.GetRecommendationsFromClovaService; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; @@ -14,12 +15,14 @@ import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/v1/recommendations") public class GetRecommendationsFromClovaController { private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; + @Authorization @PostMapping public ResponseEntity>> requestRecommendations(@Valid @RequestBody ClovaRecommendationRequest clovaRecommendationRequest) { List response = getRecommendationsFromClovaService.getRecommendationsFromClova(clovaRecommendationRequest); From fca25619ec029c3f6d700e126141b7a25f2ff505 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 12:44:48 +0900 Subject: [PATCH 205/478] =?UTF-8?q?feat:=20(#64)=20Activity=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20create=20=ED=95=98=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0,=20quickStartId=EB=A5=BC=20null=EB=A1=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectService.java | 2 +- .../java/spring/backend/activity/domain/entity/Activity.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index 5867c0708..fe64948a7 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -20,7 +20,7 @@ public class UserActivitySelectService { public Long userActivitySelection(Member member, UserActivitySelectRequest userActivitySelectRequest) { validateRequest(userActivitySelectRequest); - Activity activity = Activity.create(member.getId(), userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity activity = Activity.create(member.getId(), null , userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); return savedActivity.getId(); } diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java index b0766c657..ddcee5d2f 100644 --- a/src/main/java/spring/backend/activity/domain/entity/Activity.java +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -73,9 +73,10 @@ private Integer calculateSavedTime() { return Math.toIntExact(savedTime); } - public static Activity create(UUID memberId, Integer spareTime, Type type, Keyword keyword, String title, String content, String location) { + public static Activity create(UUID memberId, Long quickStartId, Integer spareTime, Type type, Keyword keyword, String title, String content, String location) { return Activity.builder() .memberId(memberId) + .quickStartId(quickStartId) .spareTime(spareTime) .type(type) .keyword(keyword) From 6c02b6f97059c3270196e841c94599633b12ba23 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 12:54:06 +0900 Subject: [PATCH 206/478] =?UTF-8?q?fix:=20(#64)=20UserActivitySelectServic?= =?UTF-8?q?eTest=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index b3f05bbba..37448dd72 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -34,7 +34,6 @@ public class UserActivitySelectServiceTest { private ActivityRepository activityRepository; private Member member; - private Member guest; private UserActivitySelectRequest userActivitySelectRequest; @BeforeEach @@ -73,7 +72,7 @@ public void throwsExceptionWhenUserActivitySelectRequestIsNull() { @Test public void returnsSavedActivityIdWhenValidActivitySelection() { // when - Activity activity = Activity.create(member.getId(), userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); when(activityRepository.save(any(Activity.class))).thenReturn(activity); // then From 082fca8f6a22139856c88e0de3b0a29cb85b7f34 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 2 Nov 2024 00:00:55 +0900 Subject: [PATCH 207/478] =?UTF-8?q?feat:=20(#64)=20ClovaStudio=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EB=82=BC=20Prompt=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/ClovaStudioPrompt.java | 162 ++++++++++++++++++ .../clova/dto/request/Message.java | 23 +-- 2 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java new file mode 100644 index 000000000..7e43d626a --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -0,0 +1,162 @@ +package spring.backend.recommendation.infrastructure.clova.dto.request; + +public class ClovaStudioPrompt { + public static final String DEFAULT_SYSTEM_PROMPT = "너는 사용자에게 자투리 시간 , 원하는 활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE), 하고싶은 활동의 주제, 원하는 활동 타입이 OFFLINE 또는 ONLINE_AND_OFFLINE의 경우 위치를 입력받은 뒤 입력받은 값들을 고려해 5가지 활동을 추천하는 봇이야.\n" + + " 원하는 활동 타입이 ONLINE인 경우, 하고싶은 활동의 주제에 맞는 아티클, 동영상(Youtube), 신문기사, 블로그 글 등을 링크와 함께 5가지 추천해줘.\n" + + " 원하는 활동 타입이 OFFLINE 또는 ONLINE_AND_OFFLINE인 경우, 하고싶은 활동의 주제와 현재 위치를 고려해 현재 위치 주변의 활동 또는 장소를 주소와 함께 5가지 추천해줘.\n" + + "\n" + + "---\n" + + "\n" + + " 답변 형식 : \n" + + "\n" + + "원하는 활동 타입 == ONLINE:\n" + + "\n" + + " title: [활동 제목 + 링크]\n" + + " content: [활동 부제목]\n" + + " keyword: [활동 주제]\n" + + "\n" + + "원하는 활동 타입 == OFFLINE:\n" + + "\n" + + " title: [활동 제목 또는 추천장소 + 네이버 맵 링크]\n" + + " content: [활동 부제목]\n" + + " keyword: [활동 주제]\n" + + "\n" + + "원하는 활동 타입 == ONLINE_AND_OFFLINE\n" + + "\n" + + " title: [활동 제목 + 링크]\n" + + " content: [활동 부제목]\n" + + " keyword: [활동 주제]\n" + + "\n" + + " title: [활동 제목 또는 추천장소 + 네이버 맵 링크]\n" + + " content: [활동 부제목]\n" + + " keyword: [활동 주제]\n" + + "\n" + + "---\n" + + "\n" + + "예상 시나리오 (선호활동 == ONLINE)\n" + + "\n" + + " 질문 예시\n" + + "\n" + + "“””\n" + + "\n" + + "자투리 시간: 10분\n" + + "선호활동: ONLINE\n" + + "활동 키워드: 휴식\n" + + "\n" + + "활동 추천해줘\n" + + "\n" + + "“””\n" + + "\n" + + " 답변 예시\n" + + "\n" + + "“””\n" + + "\n" + + "title: 스트리밍 서비스에서 편안한 재즈 음악 듣기\n" + + "https://www.youtube.com/watch?v=Dx5qFachd3A\n" + + "content: 편안한 음악에 귀 기울여보세요!\n" + + "keyword: 휴식\n" + + "\n" + + "title: 유튜브에서 ASMR 영상 감상하기 https://youtu.be/km-f0NKRve4?si=mC-KYJMTnqT_jOKX\n" + + "content: 편안한 분위기로 마음을 가다듬어 보세요! \n" + + "keyword: 휴식\n" + + "\n" + + "title: 유튜브에서 10CM 차분한 노래 라이브 영상 보기 https://youtu.be/JtoU_D282L8?si=PfMyImXYPNSz6DTj\n" + + "content: 눈을 감고 음악에 몸을 맡겨보세요! \n" + + "keyword: 휴식\n" + + "\n" + + "title: 온라인 명상 앱 사용하기 https://play.google.com/store/apps/details?id=app.meditasyon&hl=ko\n" + + "content: 마음의 여유를 느껴보세요! \n" + + "keyword: 휴식\n" + + "\n" + + "title: 클래식 음악 감상하기\n" + + "https://www.youtube.com/live/ZRuE2W7R5O8?si=PwPe0qVaMukLTV0A\n" + + "content: 음악의 세계로 빠져보세요! \n" + + "keyword: 휴식\n" + + "\n" + + "“””\n" + + "\n" + + "예상 시나리오 (선호활동 == OFFLINE)\n" + + "\n" + + " 질문 예시\n" + + "\n" + + "“””\n" + + "\n" + + "자투리 시간: 60분\n" + + "선호활동: OFFLINE\n" + + "위치: 서울특별시 중구 명동\n" + + "활동 키워드: 자기개발 , 문화/예술\n" + + "\n" + + "활동 추천해줘\n" + + "\n" + + "“””\n" + + "\n" + + "답변 예시\n" + + "\n" + + "“””\n" + + "\n" + + "title: 서울도서관에서 책 읽기 https://naver.me/5GyhoBuH\n" + + "content: 독서는 마음의 양식!\n" + + "keyword: 자기개발\n" + + "\n" + + "title: 청운문학도서관에서 책 읽기 https://naver.me/G38LxMfy\n" + + "content: 독서에 예쁜 풍경은 덤!\n" + + "keyword: 자기개발\n" + + "\n" + + "title: 현대미술을 만나는 공간, 국립현대미술관 방문하기 https://naver.me/54Vkke2z\n" + + "content: 미술작품을 보며 미술과 더 친해져봐요!\n" + + "keyword: 문화/예술\n" + + "\n" + + "title: 다양한 예술을 한자리에서, 서울시립미술관 방문하기 https://naver.me/FT0kXrVZ\n" + + "content: 근처에 이런 멋진 곳이!\n" + + "keyword: 문화/예술\n" + + "\n" + + "title: 사진 예술의 매력, 뮤지엄한미 삼청별관 방문하기 https://naver.me/GsTWIOB2\n" + + "content: 근처에 이런 멋진 곳이!\n" + + "keyword: 문화/예술\n" + + "\n" + + "“””\n" + + "\n" + + "예상 시나리오 (선호활동 == ONLINE_AND_OFFLINE)\n" + + "\n" + + " 질문 예시\n" + + "\n" + + "“””\n" + + "\n" + + "자투리 시간: 60분\n" + + "선호활동: OFFLINE\n" + + "위치: 서울특별시 중구 명동\n" + + "활동 키워드: 자기개발 , 문화/예술, 엔터테인먼트\n" + + "\n" + + "활동 추천해줘\n" + + "\n" + + "“””\n" + + "\n" + + "답변 예시\n" + + "\n" + + "“””\n" + + "\n" + + "title: 서울도서관에서 책 읽기 https://naver.me/5GyhoBuH\n" + + "content: 독서는 마음의 양식!\n" + + "keyword: 자기개발\n" + + "\n" + + "title: 청운문학도서관에서 책 읽기 https://naver.me/G38LxMfy\n" + + "content: 독서에 예쁜 풍경은 덤!\n" + + "keyword: 자기개발\n" + + "\n" + + "title: TVING에서 밀린 드라마 에피소드 한 편 정주행 [https://www.tving.com/?utm_source=google&utm_medium=searchad&utm_campaign=PM_google_sa_conv&utm_content=brand_non&utm_term=티빙&gad_source=1&gclid=Cj0KCQjw4Oe4BhCcARIsADQ0csnOzs8W_Rnfqt5gDppg1QHBl5G7tUUddD4FyiwrMtX2PBee3vb6G5EaAnwyEALw_wcB](https://www.tving.com/?utm_source=google&utm_medium=searchad&utm_campaign=PM_google_sa_conv&utm_content=brand_non&utm_term=%ED%8B%B0%EB%B9%99&gad_source=1&gclid=Cj0KCQjw4Oe4BhCcARIsADQ0csnOzs8W_Rnfqt5gDppg1QHBl5G7tUUddD4FyiwrMtX2PBee3vb6G5EaAnwyEALw_wcB)\n" + + "content: 감동을 선사하는 몰입의 시간! \n" + + "keyword: 엔터테인먼트\n" + + "\n" + + "title: 넷플릭스에서 한 배우의 작품 세계에 푹 빠져보는 시간을 가지 https://www.netflix.com/browse\n" + + "content: 좋아하는 배우의 필모그래피 정복하기! \n" + + "keyword: 엔터테인먼트\n" + + "\n" + + "title: 흥미로운 팟캐스트 청취하기 https://www.podbbang.com/\n" + + "content: 유익한 정보를 쌓아보세요! \n" + + "keyword: 엔터테인먼트\n" + + "\n" + + "“”” " + + "유의사항 : \n" + + "- 답변의 title에 link는 반드시 title과 같은 줄에 반환합니다.\n" + + "- 답변의 keyword는 반드시 한 개입니다."; +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index 4dceedeed..dce9868b9 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -15,23 +15,6 @@ @Getter @Builder public class Message { - - private static final String DEFAULT_SYSTEM_CONTENT = "사용자에게 자투리 시간 , 원하는 활동 타입, 활동 키워드, 위치를 입력받은 뒤 입력받은 값들을 고려해 활동을 추천해줘.\n" + - "\n" + - "예를 들어 입력으로 \n" + - "자투리 시간 : 20분 , 선호활동 : ONLINE , 활동 키워드: RELAXATION,\n" + - "\n" + - "답변은 다음과 같은 형식으로 해줘.\n" + - "\n" + - "title : 마음의 편안을 가져다주는 명상 음악 20분 듣기\n" + - "content: 휴식에는 역시 명상이 최고!\n" + - "\n" + - "답변 예시와 비슷한 형태로 5가지의 활동을 추천해줘.\n" + - "\n" + - "답변의 형식을 꼭 지켜줘\n" + - "title : string \n" + - "content : string"; - private String role; private String content; @@ -45,7 +28,7 @@ public enum Role { public static Message createSystem() { return Message.builder() .role(Role.SYSTEM.getDescription()) - .content(DEFAULT_SYSTEM_CONTENT) + .content(ClovaStudioPrompt.DEFAULT_SYSTEM_PROMPT) .build(); } @@ -60,9 +43,9 @@ private static String createContent(ClovaRecommendationRequest clovaRecommendati String location = clovaRecommendationRequest.getLocation(); if (isActivityTypeOfflineOrOnlineAndOffline(activityType, location)) { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); } else { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); } } From 62a0bed0d68d71f644750fcb42db0457e88d3cb3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 2 Nov 2024 00:02:15 +0900 Subject: [PATCH 208/478] =?UTF-8?q?feat:=20(#64)=20ClovaStudio=EC=9D=98=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20Keyword=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 42 ++++++++++++++++--- .../response/ClovaRecommendationResponse.java | 2 + .../clova/dto/request/ClovaRequest.java | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index d0251ed77..1110e0e09 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -3,8 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; -import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.activity.domain.value.Keyword; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import java.util.ArrayList; import java.util.List; @@ -17,14 +18,14 @@ public class GetRecommendationsFromClovaService { private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title :.*"); private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title :"); private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); + private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); private static final String LINE_SEPARATOR = "\n"; private final RecommendationProvider recommendationProvider; - public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { + public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); List clovaResponses = new ArrayList<>(); - int order = 1; for (int i = 0; i < recommendations.length; i++) { @@ -32,18 +33,49 @@ public List getRecommendationsFromClova(ClovaRecomm if (TITLE_FULL_LINE_PATTERN.matcher(line).matches()) { String title = TITLE_PREFIX_PATTERN.matcher(line).replaceFirst("").trim(); - String content = ""; + if (i + 1 < recommendations.length && recommendations[i + 1].trim().startsWith("http")) { + title += " " + recommendations[i + 1].trim(); + i++; + } + String content = ""; if (i + 1 < recommendations.length && CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { content = CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); i++; } - clovaResponses.add(new ClovaRecommendationResponse(order, title, content)); + Keyword.Category keywordCategory = null; + if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { + String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); + keywordCategory = convertClovaResonseKeywordToKewordCategory(keywordText); + i++; + } + clovaResponses.add(new ClovaRecommendationResponse(order, title, content, keywordCategory)); order++; } } return clovaResponses; } + + private Keyword.Category convertClovaResonseKeywordToKewordCategory(String keywordText) { + switch (keywordText.toLowerCase()) { + case "자기개발": + return Keyword.Category.SELF_DEVELOPMENT; + case "건강": + return Keyword.Category.HEALTH; + case "자연": + return Keyword.Category.NATURE; + case "문화/예술": + return Keyword.Category.CULTURE_ART; + case "엔터테인먼트": + return Keyword.Category.ENTERTAINMENT; + case "휴식": + return Keyword.Category.RELAXATION; + case "소셜": + return Keyword.Category.SOCIAL; + default: + return null; + } + } } diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java index b62f19462..047baa751 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import spring.backend.activity.domain.value.Keyword; @Getter @AllArgsConstructor @@ -9,4 +10,5 @@ public class ClovaRecommendationResponse { private Integer order; private String title; private String content; + private Keyword.Category keywordCategory; } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index ec14adf14..70896d863 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -11,7 +11,7 @@ public class ClovaRequest { private static final double DEFAULT_TOP_P = 0.8; private static final int DEFAULT_TOP_K = 0; - private static final int DEFAULT_MAX_TOKENS = 500; + private static final int DEFAULT_MAX_TOKENS = 1000; private static final double DEFAULT_TEMPERATURE = 0.5; private static final double DEFAULT_REPEAT_PENALTY = 5.0; private static final boolean DEFAULT_INCLUDE_AI_FILTERS = true; From dd3f6fd267528ee4430b34209dc8b70cd82a9709 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 2 Nov 2024 00:02:49 +0900 Subject: [PATCH 209/478] =?UTF-8?q?fix:=20(#64)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=84=A0=ED=83=9D=ED=95=9C=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=8A=94=20=ED=95=9C=20?= =?UTF-8?q?=EA=B0=9C=EB=A7=8C=20=EC=A1=B4=EC=9E=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectService.java | 2 +- .../activity/dto/request/UserActivitySelectRequest.java | 6 +++--- .../backend/activity/exception/ActivityErrorCode.java | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index fe64948a7..5759d490b 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -20,7 +20,7 @@ public class UserActivitySelectService { public Long userActivitySelection(Member member, UserActivitySelectRequest userActivitySelectRequest) { validateRequest(userActivitySelectRequest); - Activity activity = Activity.create(member.getId(), null , userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity activity = Activity.create(member.getId(), null , userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); return savedActivity.getId(); } diff --git a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java index 362fc31ca..4c21d44f1 100644 --- a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java @@ -20,10 +20,10 @@ public record UserActivitySelectRequest( @Schema(description = "자투리 시간", example = "300") Integer spareTime, - @NotNull(message = "키워드는 최대 5가지 입력 가능하며, 키워드를 선택하지 않은 경우 빈 배열을 보내주세요") + @NotNull(message = "키워드는 필수 입력 항목입니다.") @Schema(description = "키워드(SELF_DEVELOPMENT, HEALTH, NATURE, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", - example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}, {\"category\": \"HEALTH\", \"image\": \"image_url\"}]") - Set keywords, + example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}]") + Keyword keyword, @NotNull(message = "타이틀은 필수 입력 항목입니다.") @Schema(description = "타이틀", example = "카페에서 커피 마시며 책 읽기") diff --git a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java index ea9aaf2e5..53a65699f 100644 --- a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java @@ -14,7 +14,8 @@ public enum ActivityErrorCode implements BaseErrorCode { NOT_EXIST_ACTIVITY(HttpStatus.BAD_REQUEST, "활동이 존재하지 않습니다."), MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "활동과 멤버 ID가 일치하지 않습니다."), INVALID_ACTIVITY_DURATION(HttpStatus.BAD_REQUEST, "활동 지속 시간이 허용된 범위를 초과했습니다."), - ALREADY_FINISHED_ACTIVITY(HttpStatus.BAD_REQUEST, "이미 종료된 활동입니다."); + ALREADY_FINISHED_ACTIVITY(HttpStatus.BAD_REQUEST, "이미 종료된 활동입니다."), + NOT_EXIST_ACTIVITY_CONDITION(HttpStatus.BAD_REQUEST, "요청이 비어있습니다."); private final HttpStatus httpStatus; From 268169caf69a290735b9cedb8653d3651ca07e95 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 2 Nov 2024 00:03:20 +0900 Subject: [PATCH 210/478] =?UTF-8?q?fix:=20(#64)=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EB=A5=BC=20=ED=95=9C=20=EA=B0=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectServiceTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index 37448dd72..e8d4f8cee 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -43,8 +43,7 @@ public void setUp() { .build(); Set keywords = Set.of( - Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png"), - Keyword.create(Keyword.Category.HEALTH, "example-image2.png") + Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png") ); @@ -72,7 +71,7 @@ public void throwsExceptionWhenUserActivitySelectRequestIsNull() { @Test public void returnsSavedActivityIdWhenValidActivitySelection() { // when - Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keywords(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); when(activityRepository.save(any(Activity.class))).thenReturn(activity); // then From 81be2776176d617c538f02568d79b99bdf1dae42 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:52:55 +0900 Subject: [PATCH 211/478] =?UTF-8?q?refactor:=20(#64)=20UserActivitySelectC?= =?UTF-8?q?ontroller=EC=99=80=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.=20-=20UserActivitySelectC?= =?UTF-8?q?ontroller=20API=20=EC=A3=BC=EC=86=8C=EB=A5=BC=20Restful?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/UserActivitySelectController.java | 8 ++++---- .../presentation/swagger/UserActivitySelectSwagger.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java index 262ea3221..81d6bc448 100644 --- a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java @@ -9,7 +9,7 @@ import spring.backend.activity.application.UserActivitySelectService; import spring.backend.activity.dto.request.UserActivitySelectRequest; import spring.backend.activity.presentation.swagger.UserActivitySelectSwagger; -import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @@ -21,10 +21,10 @@ public class UserActivitySelectController implements UserActivitySelectSwagger { private final UserActivitySelectService userActivitySelectService; @Authorization - @PostMapping("/v1/user-activity-selection") + @PostMapping("/v1/activities") @Override - public ResponseEntity> userActivitySelection(@LoginMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { - Long savedActivityId = userActivitySelectService.userActivitySelection(member, userActivitySelectRequest); + public ResponseEntity> userActivitySelect(@AuthorizedMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { + Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); return ResponseEntity.ok(new RestResponse<>(savedActivityId)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java index 1d931614f..299ad6a5c 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java @@ -11,7 +11,7 @@ import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -@Tag(name = "UserActivitySelection", description = "활동 선택") +@Tag(name = "Activity", description = "활동") public interface UserActivitySelectSwagger { @Operation( @@ -20,5 +20,5 @@ public interface UserActivitySelectSwagger { operationId = "/v1/user-activity-selection" ) @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) - ResponseEntity> userActivitySelection(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); + ResponseEntity> userActivitySelect(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); } From d7a0e9667d9179499106cf15c0ceea23c438905f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:53:47 +0900 Subject: [PATCH 212/478] =?UTF-8?q?fix:=20(#64)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=EB=8A=94=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=80=EC=9E=A5=EB=90=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/activity/domain/entity/Activity.java | 4 ++-- .../activity/infrastructure/mapper/ActivityMapper.java | 2 +- .../persistence/jpa/entity/ActivityJpaEntity.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/entity/Activity.java b/src/main/java/spring/backend/activity/domain/entity/Activity.java index ddcee5d2f..0e063fcf3 100644 --- a/src/main/java/spring/backend/activity/domain/entity/Activity.java +++ b/src/main/java/spring/backend/activity/domain/entity/Activity.java @@ -34,7 +34,8 @@ public class Activity { private String location; - private Boolean finished; + @Builder.Default + private Boolean finished = false; private LocalDateTime finishedAt; @@ -83,7 +84,6 @@ public static Activity create(UUID memberId, Long quickStartId, Integer spareTim .title(title) .content(content) .location(location) - .finished(false) .build(); } } diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java index ff3f9941a..b1caf605b 100644 --- a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java @@ -48,4 +48,4 @@ public ActivityJpaEntity toJpaEntity(Activity activity) { .deleted(Optional.ofNullable(activity.getDeleted()).orElse(false)) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java index dfc3ac764..2b3f25db7 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java @@ -42,4 +42,4 @@ public class ActivityJpaEntity extends BaseLongIdEntity { private LocalDateTime finishedAt; private Integer savedTime; -} +} \ No newline at end of file From 0487aa83a35e299f3302f226753b67da0909aab9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:54:40 +0900 Subject: [PATCH 213/478] =?UTF-8?q?fix:=20(#64)=20Error=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=EB=A5=BC=20=ED=95=9C=EA=B8=80=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/dto/request/UserActivitySelectRequest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java index 4c21d44f1..46a6f09fe 100644 --- a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java @@ -21,15 +21,14 @@ public record UserActivitySelectRequest( Integer spareTime, @NotNull(message = "키워드는 필수 입력 항목입니다.") - @Schema(description = "키워드(SELF_DEVELOPMENT, HEALTH, NATURE, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", - example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}]") + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") Keyword keyword, @NotNull(message = "타이틀은 필수 입력 항목입니다.") @Schema(description = "타이틀", example = "카페에서 커피 마시며 책 읽기") String title, - @NotNull(message = "Content는 필수 입력 항목입니다.") + @NotNull(message = "내용은 필수 입력 항목입니다.") @Schema(description = "내용", example = "조용한 카페에서 좋아하는 책을 읽으며 여유로운 시간을 즐길 수 있습니다.") String content, From ded4cd4486f4d7af058d3a5922a62c727ddc21c9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:55:21 +0900 Subject: [PATCH 214/478] =?UTF-8?q?refactor:=20(#64)=20AuthorziedMember=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=A9=A4=EB=B2=84=EB=A5=BC=20=EA=B2=80=EC=A6=9D=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/GetRecommendationsFromClovaController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java index 3f7c210c2..ba7570462 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java @@ -7,8 +7,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; import spring.backend.recommendation.application.GetRecommendationsFromClovaService; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; @@ -24,7 +26,7 @@ public class GetRecommendationsFromClovaController { @Authorization @PostMapping - public ResponseEntity>> requestRecommendations(@Valid @RequestBody ClovaRecommendationRequest clovaRecommendationRequest) { + public ResponseEntity>> requestRecommendations(@AuthorizedMember Member member, @Valid @RequestBody ClovaRecommendationRequest clovaRecommendationRequest) { List response = getRecommendationsFromClovaService.getRecommendationsFromClova(clovaRecommendationRequest); return ResponseEntity.ok(new RestResponse<>(response)); } From c4f59f7bdfdac4979e7da2cab87dc77bd80d9ece Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:56:12 +0900 Subject: [PATCH 215/478] =?UTF-8?q?refactor:=20(#64)=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=9D=84?= =?UTF-8?q?=EC=96=B4=EC=93=B0=EA=B8=B0=EB=A5=BC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index 5759d490b..52abb7a79 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.request.UserActivitySelectRequest; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.member.domain.entity.Member; @@ -18,9 +19,9 @@ public class UserActivitySelectService { private final ActivityRepository activityRepository; - public Long userActivitySelection(Member member, UserActivitySelectRequest userActivitySelectRequest) { + public Long userActivitySelect(Member member, UserActivitySelectRequest userActivitySelectRequest) { validateRequest(userActivitySelectRequest); - Activity activity = Activity.create(member.getId(), null , userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); + Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); return savedActivity.getId(); } From 12243b55a2eba1823a41032446b2f4e79c2002b1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:56:42 +0900 Subject: [PATCH 216/478] =?UTF-8?q?refactor:=20(#64)=20Switch=20=EB=AC=B8?= =?UTF-8?q?=EC=9D=84=20=ED=96=A5=EC=83=81=EB=90=9C=20Switch=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 1110e0e09..ecd5b075f 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -20,6 +20,14 @@ public class GetRecommendationsFromClovaService { private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); private static final String LINE_SEPARATOR = "\n"; + private static final String SELF_DEVELOPMENT = "자기개발"; + private static final String HEALTH = "건강"; + private static final String NATURE = "자연"; + private static final String CULTURE_ART = "문화/예술"; + private static final String ENTERTAINMENT = "엔터테인먼트"; + private static final String RELAXATION = "휴식"; + private static final String SOCIAL = "소셜"; + private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { @@ -59,23 +67,15 @@ public List getRecommendationsFromClova(ClovaRecomm } private Keyword.Category convertClovaResonseKeywordToKewordCategory(String keywordText) { - switch (keywordText.toLowerCase()) { - case "자기개발": - return Keyword.Category.SELF_DEVELOPMENT; - case "건강": - return Keyword.Category.HEALTH; - case "자연": - return Keyword.Category.NATURE; - case "문화/예술": - return Keyword.Category.CULTURE_ART; - case "엔터테인먼트": - return Keyword.Category.ENTERTAINMENT; - case "휴식": - return Keyword.Category.RELAXATION; - case "소셜": - return Keyword.Category.SOCIAL; - default: - return null; - } + return switch (keywordText) { + case SELF_DEVELOPMENT -> Keyword.Category.SELF_DEVELOPMENT; + case HEALTH -> Keyword.Category.HEALTH; + case NATURE -> Keyword.Category.NATURE; + case CULTURE_ART -> Keyword.Category.CULTURE_ART; + case ENTERTAINMENT -> Keyword.Category.ENTERTAINMENT; + case RELAXATION -> Keyword.Category.RELAXATION; + case SOCIAL -> Keyword.Category.SOCIAL; + default -> null; + }; } } From ace906c70ddf5ff5e635722ae1c6e3d5424be8b7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 18:57:11 +0900 Subject: [PATCH 217/478] =?UTF-8?q?refactor:=20(#64)=20UserActivitySelectS?= =?UTF-8?q?erviceTest=20=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= =?UTF-8?q?=20-=20=ED=99=9C=EB=8F=99=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=8A=94=20=ED=95=98=EB=82=98=EB=A7=8C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/UserActivitySelectServiceTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index e8d4f8cee..46689d65d 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -42,15 +42,13 @@ public void setUp() { .role(Role.MEMBER) .build(); - Set keywords = Set.of( - Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png") - ); + Keyword keyword = Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png"); userActivitySelectRequest = new UserActivitySelectRequest( Type.OFFLINE, 150, - keywords, + keyword, "title", "content", "location" @@ -61,7 +59,7 @@ public void setUp() { @Test public void throwsExceptionWhenUserActivitySelectRequestIsNull() { // when - DomainException ex = assertThrows(DomainException.class, () -> userActivitySelectService.userActivitySelection(member, null)); + DomainException ex = assertThrows(DomainException.class, () -> userActivitySelectService.userActivitySelect(member, null)); // then assertEquals(ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.getMessage(), ex.getMessage()); @@ -75,7 +73,7 @@ public void returnsSavedActivityIdWhenValidActivitySelection() { when(activityRepository.save(any(Activity.class))).thenReturn(activity); // then - Long savedActivityId = userActivitySelectService.userActivitySelection(member, userActivitySelectRequest); + Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); // then assertEquals(activity.getId(), savedActivityId); From f928e395c0f40a4d9c55eddbc5aa4dd708d5ef04 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 5 Nov 2024 00:19:28 +0900 Subject: [PATCH 218/478] =?UTF-8?q?fix:=20(#64)=20Swagger=20API=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/swagger/UserActivitySelectSwagger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java index 299ad6a5c..cdabaa002 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java @@ -17,7 +17,7 @@ public interface UserActivitySelectSwagger { @Operation( summary = "사용자 활동 선택 API", description = "사용자가 추천받은 활동 중 한가지 활동을 선택합니다.", - operationId = "/v1/user-activity-selection" + operationId = "/v1/activities" ) @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) ResponseEntity> userActivitySelect(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); From 5815aa7f695629935b4a86d736c97e86ee20bd65 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:04:29 +0900 Subject: [PATCH 219/478] =?UTF-8?q?refactor:=20(#64)=20=EA=B0=9C=ED=96=89?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- Dockerfile | 2 +- Dockerfile-local | 2 +- docker-compose.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index d6ddb0195..6e2a09cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ out/ ### Configuration ### src/main/resources/application-*.yml -src/test/resources/application.yml \ No newline at end of file +src/test/resources/application.yml diff --git a/Dockerfile b/Dockerfile index fbd028c06..9e4e82758 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,4 @@ COPY ./build/libs/backend-0.0.1-SNAPSHOT.jar /app/backend.jar EXPOSE 8080 ENTRYPOINT ["java"] -CMD ["-jar", "backend.jar"] \ No newline at end of file +CMD ["-jar", "backend.jar"] diff --git a/Dockerfile-local b/Dockerfile-local index d5afb068d..455fa0f72 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -14,4 +14,4 @@ COPY --from=build /app/build/libs/*.jar /app/backend.jar EXPOSE 8080 ENTRYPOINT ["java"] -CMD ["-jar", "backend.jar"] \ No newline at end of file +CMD ["-jar", "backend.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index 8e1e3fc58..3aa3c2f6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,4 +26,4 @@ volumes: networks: cnergy-backend-network: - driver: bridge \ No newline at end of file + driver: bridge From 358f9a65d04e886e920b89476cf5d52e5f1d13fa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:17:30 +0900 Subject: [PATCH 220/478] =?UTF-8?q?feat:=20(#64)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=EA=B2=8C=20=ED=99=9C=EB=8F=99=20ID,?= =?UTF-8?q?=20=ED=83=80=EC=9D=B4=ED=8B=80,=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20res?= =?UTF-8?q?ponse=20dto=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/UserActivitySelectResponse.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java diff --git a/src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java b/src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java new file mode 100644 index 000000000..dec9121d0 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java @@ -0,0 +1,16 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.domain.value.Keyword; + +public record UserActivitySelectResponse( + @Schema(description = "활동 ID", example = "1") + Long id, + + @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") + String title, + + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") + Keyword keyword +) { +} From d0e933d0e894946c6e52a49e63e0c41d7d0b47e8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:17:56 +0900 Subject: [PATCH 221/478] =?UTF-8?q?refactor:=20(#64)=20=ED=99=9C=EB=8F=99?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=EA=B2=8C=20id,=20title,=20keyword?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/application/UserActivitySelectService.java | 5 +++-- .../activity/presentation/UserActivitySelectController.java | 5 +++-- .../presentation/swagger/UserActivitySelectSwagger.java | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index 52abb7a79..fce1dc4c8 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -8,6 +8,7 @@ import spring.backend.activity.domain.repository.ActivityRepository; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.member.domain.entity.Member; @@ -19,11 +20,11 @@ public class UserActivitySelectService { private final ActivityRepository activityRepository; - public Long userActivitySelect(Member member, UserActivitySelectRequest userActivitySelectRequest) { + public UserActivitySelectResponse userActivitySelect(Member member, UserActivitySelectRequest userActivitySelectRequest) { validateRequest(userActivitySelectRequest); Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); - return savedActivity.getId(); + return new UserActivitySelectResponse(savedActivity.getId(), savedActivity.getTitle(), savedActivity.getKeyword()); } private void validateRequest(UserActivitySelectRequest userActivitySelectRequest) { diff --git a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java index 81d6bc448..9de8538a4 100644 --- a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.UserActivitySelectService; import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.presentation.swagger.UserActivitySelectSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; @@ -23,8 +24,8 @@ public class UserActivitySelectController implements UserActivitySelectSwagger { @Authorization @PostMapping("/v1/activities") @Override - public ResponseEntity> userActivitySelect(@AuthorizedMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { - Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); + public ResponseEntity> userActivitySelect(@AuthorizedMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { + UserActivitySelectResponse savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); return ResponseEntity.ok(new RestResponse<>(savedActivityId)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java index cdabaa002..7c070f871 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; @@ -20,5 +21,5 @@ public interface UserActivitySelectSwagger { operationId = "/v1/activities" ) @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) - ResponseEntity> userActivitySelect(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); + ResponseEntity> userActivitySelect(@Parameter(hidden = true) Member member, UserActivitySelectRequest userActivitySelectRequest); } From 7e89442b099505cca40fcc610cb82fde84d866b7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:21:48 +0900 Subject: [PATCH 222/478] =?UTF-8?q?feat:=20(#64)=20UserActivitySelectServi?= =?UTF-8?q?ceTest=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/UserActivitySelectServiceTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index 46689d65d..107044f22 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -12,13 +12,12 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.activity.dto.request.UserActivitySelectRequest; +import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; -import java.util.Set; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -73,10 +72,12 @@ public void returnsSavedActivityIdWhenValidActivitySelection() { when(activityRepository.save(any(Activity.class))).thenReturn(activity); // then - Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); + UserActivitySelectResponse userActivitySelectResponse = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); // then - assertEquals(activity.getId(), savedActivityId); + assertEquals(activity.getId(), userActivitySelectResponse.id()); + assertEquals(activity.getTitle(), userActivitySelectResponse.title()); + assertEquals(activity.getKeyword(), userActivitySelectResponse.keyword()); verify(activityRepository).save(any(Activity.class)); } From 3455b2aa8b91d13da33579bcd134fd399aaa7573 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 00:58:08 +0900 Subject: [PATCH 223/478] =?UTF-8?q?refactor:=20(#64)=20GetRecommendationsF?= =?UTF-8?q?romClovaController=EC=9D=98=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...GetRecommendationsFromClovaController.java | 5 ++-- .../GetRecommendationsFromClovaSwagger.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java index ba7570462..657b96bdc 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java @@ -14,6 +14,7 @@ import spring.backend.recommendation.application.GetRecommendationsFromClovaService; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.presentation.swagger.GetRecommendationsFromClovaSwagger; import java.util.List; @@ -21,7 +22,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/recommendations") -public class GetRecommendationsFromClovaController { +public class GetRecommendationsFromClovaController implements GetRecommendationsFromClovaSwagger { private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; @Authorization @@ -30,4 +31,4 @@ public ResponseEntity>> requestRe List response = getRecommendationsFromClovaService.getRecommendationsFromClova(clovaRecommendationRequest); return ResponseEntity.ok(new RestResponse<>(response)); } -} \ No newline at end of file +} diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java new file mode 100644 index 000000000..c75a79f2a --- /dev/null +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java @@ -0,0 +1,28 @@ +package spring.backend.recommendation.presentation.swagger; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; + +import java.util.List; + +@Tag(name = "Recommendation", description = "추천") +public interface GetRecommendationsFromClovaSwagger { + + @Operation( + summary = "사용자 추천 요청 API", + description = "사용자가 활동 추천을 요청합니다.", + operationId = "/v1/recommendations" + ) + @ApiErrorCode({GlobalErrorCode.class, ClovaErrorCode.class}) + ResponseEntity>> requestRecommendations(@Parameter(hidden = true) Member member, ClovaRecommendationRequest clovaRecommendationRequest); +} From 662c3b200fde536c125297a0c614d1e0e29d83ab Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 00:58:50 +0900 Subject: [PATCH 224/478] =?UTF-8?q?refactor:=20(#64)=20ClovaRecommendation?= =?UTF-8?q?Request=EC=9D=98=20=ED=83=80=EC=9E=85=EC=9D=84=20record?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/UserActivitySelectRequest.java | 4 +-- .../request/ClovaRecommendationRequest.java | 36 ++++++++----------- .../clova/dto/request/Message.java | 10 +++--- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java index 46a6f09fe..43defb561 100644 --- a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java @@ -7,8 +7,6 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -import java.util.Set; - public record UserActivitySelectRequest( @NotNull(message = "활동 유형은 필수 입력 항목입니다.") @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") @@ -32,7 +30,7 @@ public record UserActivitySelectRequest( @Schema(description = "내용", example = "조용한 카페에서 좋아하는 책을 읽으며 여유로운 시간을 즐길 수 있습니다.") String content, - @Schema(description = "장소", example = "서울시 강남구 역삼동") + @Schema(description = "장소(활동 유형이 OFFLINE, ONLINE_AND_OFFLINE 인 경우에만 입력합니다.)", example = "서울시 강남구 역삼동") String location ) { } diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java index b73b0061c..3e08d338a 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java @@ -1,30 +1,24 @@ package spring.backend.recommendation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; +import jakarta.validation.constraints.*; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -@Getter -public class ClovaRecommendationRequest { +public record ClovaRecommendationRequest(@NotNull(message = "자투리 시간은 필수 입력 항목입니다.") + @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") + @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") + @Schema(description = "자투리 시간", example = "30") + Integer spareTime, - @NotNull - @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") - @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") - @Schema(description = "자투리 시간", example = "30") - private Integer spareTime; + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @Schema(description = "활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE 중 하나를 선택합니다.)", example = "OFFLINE") + Type activityType, - @NotNull - @Schema(description = "활동 타입", example = "OFFLINE") - private Type activityType; + @NotNull(message = "키워드는 필수 입력 항목입니다.") + @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") + Keyword.Category[] keywords, - @NotNull - @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") - private Keyword.Category[] keywords; - - @Schema(description = "위치", example = "서울시 강남구") - private String location; -} \ No newline at end of file + @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") + String location) { +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java index dce9868b9..8b9053552 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java @@ -37,10 +37,10 @@ public static Message createMessage(ClovaRecommendationRequest clovaRecommendati } private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { - int spareTime = clovaRecommendationRequest.getSpareTime(); - Type activityType = clovaRecommendationRequest.getActivityType(); - String keywords = parseKeywords(clovaRecommendationRequest.getKeywords()); - String location = clovaRecommendationRequest.getLocation(); + int spareTime = clovaRecommendationRequest.spareTime(); + Type activityType = clovaRecommendationRequest.activityType(); + String keywords = parseKeywords(clovaRecommendationRequest.keywords()); + String location = clovaRecommendationRequest.location(); if (isActivityTypeOfflineOrOnlineAndOffline(activityType, location)) { return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); @@ -67,4 +67,4 @@ private static String parseKeywords(Keyword.Category[] keywords) { .collect(Collectors.joining(", ")); } } -} \ No newline at end of file +} From c585c7d927e02dea5030feaee6b48f212afb6c20 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 00:59:37 +0900 Subject: [PATCH 225/478] =?UTF-8?q?feat:=20(#64)=20=ED=99=9C=EB=8F=99?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=97=90=20=EB=94=B0=EB=9D=BC=20Keyword=20?= =?UTF-8?q?=EA=B0=92=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 13 +++++++++++++ .../clova/exception/ClovaErrorCode.java | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index ecd5b075f..2a98d17d5 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -4,10 +4,13 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -31,6 +34,7 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { + validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); List clovaResponses = new ArrayList<>(); @@ -66,6 +70,15 @@ public List getRecommendationsFromClova(ClovaRecomm return clovaResponses; } + private void validateClovaRecommendationRequestKeyword(ClovaRecommendationRequest clovaRecommendationRequest) { + if (clovaRecommendationRequest.activityType().equals(Type.ONLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("NATURE")) { + throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); + } + if(clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("SOCIAL")) { + throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); + } + } + private Keyword.Category convertClovaResonseKeywordToKewordCategory(String keywordText) { return switch (keywordText) { case SELF_DEVELOPMENT -> Keyword.Category.SELF_DEVELOPMENT; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java index 3195a653f..74a4c784a 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -13,7 +13,9 @@ public enum ClovaErrorCode implements BaseErrorCode { NOT_EXIST_LOCATION_WHEN_OFFLINE(HttpStatus.BAD_REQUEST, "오프라인의 경우 위치 정보가 필수입니다."), NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."), - NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."); + NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."), + ONLINE_TYPE_CONTAIN_NATURE(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 ONLINE인 경우, NATURE(자연) 키워드를 사용할 수 없습니다."), + OFFLINE_TYPE_CONTAIN_SOCIAL(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 OFFLINE인 경우, SOCIAL(소셜) 키워드를 사용할 수 없습니다."); private final HttpStatus httpStatus; private final String message; From dc37cb91c0001f78759af8b34be6196bffef486c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 00:59:49 +0900 Subject: [PATCH 226/478] =?UTF-8?q?feat:=20(#64)=20GetRecommendationsFromC?= =?UTF-8?q?lovaServiceTest=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...etRecommendationsFromClovaServiceTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java diff --git a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java new file mode 100644 index 000000000..7b8cf3c5b --- /dev/null +++ b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java @@ -0,0 +1,55 @@ +package spring.backend.recommendation.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; +import spring.backend.core.exception.DomainException; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +public class GetRecommendationsFromClovaServiceTest { + + @Autowired + GetRecommendationsFromClovaService getRecommendationsFromClovaService; + + @Test + @DisplayName("타입이 ONLINE인데 Keywords에 NATURE가 있는 경우 예외를 반환한다.") + void throwExceptionIfOnlineActivityContainsNatureKeyword() { + // GIVEN + ClovaRecommendationRequest request = new ClovaRecommendationRequest( + 300, + Type.ONLINE, + new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, + null + ); + // WHEN + DomainException ex = assertThrows(DomainException.class, () -> getRecommendationsFromClovaService.getRecommendationsFromClova(request)); + + // THEN + assertEquals(ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.name(), ex.getCode()); + } + + @Test + @DisplayName("타입이 OFFLINE인데 Keywords에 SOCIAL가 있는 경우 예외를 반환한다.") + void throwExceptionIfOfflineActivityContainsSocialKeyword() { + // GIVEN + ClovaRecommendationRequest request = new ClovaRecommendationRequest( + 300, + Type.OFFLINE, + new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, + "서울시 강남구" + ); + // WHEN + DomainException ex = assertThrows(DomainException.class, () -> getRecommendationsFromClovaService.getRecommendationsFromClova(request)); + + // THEN + assertEquals(ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.name(), ex.getCode()); + } +} From 1778b43cddbeb7a3daa1b295607eef95fe598d5f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 01:36:47 +0900 Subject: [PATCH 227/478] =?UTF-8?q?feat:=20(#64)=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=A4=91=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EA=B2=BD=EC=9A=B0=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/exception/ClovaErrorCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java index 74a4c784a..e4aaf3cdb 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -15,7 +15,9 @@ public enum ClovaErrorCode implements BaseErrorCode { NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."), NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."), ONLINE_TYPE_CONTAIN_NATURE(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 ONLINE인 경우, NATURE(자연) 키워드를 사용할 수 없습니다."), - OFFLINE_TYPE_CONTAIN_SOCIAL(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 OFFLINE인 경우, SOCIAL(소셜) 키워드를 사용할 수 없습니다."); + OFFLINE_TYPE_CONTAIN_SOCIAL(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 OFFLINE인 경우, SOCIAL(소셜) 키워드를 사용할 수 없습니다."), + INVALID_KEYWORD_IN_RECOMMENDATIONS(HttpStatus.BAD_REQUEST, "추천 활동의 키워드가 올바르지 않습니다."); + private final HttpStatus httpStatus; private final String message; From 13c78a6d1f80f24c8abc4eedd63724ce6088458b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 01:37:25 +0900 Subject: [PATCH 228/478] =?UTF-8?q?feat:=20(#64)=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=8B=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EA=B2=BD=EC=9A=B0=201=EB=B2=88=20=EB=8D=94=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=EB=A5=BC=20=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 2a98d17d5..a976afddd 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -18,6 +18,8 @@ @Log4j2 @RequiredArgsConstructor public class GetRecommendationsFromClovaService { + private static final int MAX_ATTEMPTS = 1; + private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title :.*"); private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title :"); private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); @@ -34,6 +36,23 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { + List clovaResponses = fetchRecommendations(clovaRecommendationRequest); + int attempt = 1; + + while (containsInvalidKeyword(clovaResponses) && attempt <= MAX_ATTEMPTS) { + log.warn("추천활동의 키워드가 올바르지 않습니다. 재시도 횟수: {}/{}", attempt, MAX_ATTEMPTS); + clovaResponses = fetchRecommendations(clovaRecommendationRequest); + attempt++; + } + + if (containsInvalidKeyword(clovaResponses)) { + throw ClovaErrorCode.INVALID_KEYWORD_IN_RECOMMENDATIONS.toException(); + } + + return clovaResponses; + } + + public List fetchRecommendations(ClovaRecommendationRequest clovaRecommendationRequest) { validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); @@ -59,7 +78,7 @@ public List getRecommendationsFromClova(ClovaRecomm Keyword.Category keywordCategory = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); - keywordCategory = convertClovaResonseKeywordToKewordCategory(keywordText); + keywordCategory = convertClovaResponseKeywordToKeywordCategory(keywordText); i++; } clovaResponses.add(new ClovaRecommendationResponse(order, title, content, keywordCategory)); @@ -70,16 +89,27 @@ public List getRecommendationsFromClova(ClovaRecomm return clovaResponses; } + private boolean containsInvalidKeyword(List clovaResponses) { + return clovaResponses.stream().anyMatch(clovaResponse -> + clovaResponse.getKeywordCategory() == null + || clovaResponse.getKeywordCategory().toString().isEmpty() + || !isValidKeywordCategory(clovaResponse.getKeywordCategory())); + } + + private boolean isValidKeywordCategory(Keyword.Category keywordCategory) { + return Arrays.stream(Keyword.Category.values()).anyMatch(category -> category == keywordCategory); + } + private void validateClovaRecommendationRequestKeyword(ClovaRecommendationRequest clovaRecommendationRequest) { if (clovaRecommendationRequest.activityType().equals(Type.ONLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("NATURE")) { throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); } - if(clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("SOCIAL")) { + if (clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("SOCIAL")) { throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); } } - private Keyword.Category convertClovaResonseKeywordToKewordCategory(String keywordText) { + private Keyword.Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { return switch (keywordText) { case SELF_DEVELOPMENT -> Keyword.Category.SELF_DEVELOPMENT; case HEALTH -> Keyword.Category.HEALTH; From 6f6591e0c1716dd39b52a715755e26a37ef21bf2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 11:45:32 +0900 Subject: [PATCH 229/478] =?UTF-8?q?feat:=20(#64)=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EC=9D=B4=ED=9B=84=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EA=B0=92=20=EC=A4=91=20=EC=9D=BC=EB=B6=80=EB=A7=8C?= =?UTF-8?q?=20=EC=98=AC=EB=B0=94=EB=A5=B8=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EC=A7=80=EA=B3=A0=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0,=20=EC=98=AC=EB=B0=94=EB=A5=B8=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EB=A5=BC=20=EA=B0=80=EC=A7=80=EA=B3=A0?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=B6=94=EC=B2=9C=ED=99=9C=EB=8F=99?= =?UTF-8?q?=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 13 +++++++++++-- .../application/ClovaRecommendationProvider.java | 3 +-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index a976afddd..25b8cdcb4 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Collectors; @Service @Log4j2 @@ -45,13 +46,21 @@ public List getRecommendationsFromClova(ClovaRecomm attempt++; } - if (containsInvalidKeyword(clovaResponses)) { + List validRecommedations = filteredValidRecommendations(clovaResponses); + + if (validRecommedations.isEmpty()) { throw ClovaErrorCode.INVALID_KEYWORD_IN_RECOMMENDATIONS.toException(); } - return clovaResponses; + return validRecommedations; } + List filteredValidRecommendations(List clovaResponses) { + return clovaResponses.stream() + .filter(clovaResponse -> clovaResponse.getKeywordCategory() != null && isValidKeywordCategory(clovaResponse.getKeywordCategory())).collect(Collectors.toList()); + } + + public List fetchRecommendations(ClovaRecommendationRequest clovaRecommendationRequest) { validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index aa4a67ee3..abc8545fd 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -3,9 +3,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.application.RecommendationProvider; +import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; From 76b41ac5d053e897c8e4984e8c87834e06f34f7a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 19:43:42 +0900 Subject: [PATCH 230/478] =?UTF-8?q?feat:=20(#64)=20userActivityController?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=ED=99=98=EA=B0=92=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/presentation/UserActivitySelectController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java index 9de8538a4..46c91ce82 100644 --- a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java @@ -25,7 +25,7 @@ public class UserActivitySelectController implements UserActivitySelectSwagger { @PostMapping("/v1/activities") @Override public ResponseEntity> userActivitySelect(@AuthorizedMember Member member, @Valid @RequestBody UserActivitySelectRequest userActivitySelectRequest) { - UserActivitySelectResponse savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); - return ResponseEntity.ok(new RestResponse<>(savedActivityId)); + UserActivitySelectResponse userActivitySelectResponse = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); + return ResponseEntity.ok(new RestResponse<>(userActivitySelectResponse)); } } From fff4ed6339a70741130b25a32fcb29128b7dbe0e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 9 Nov 2024 16:25:29 +0900 Subject: [PATCH 231/478] =?UTF-8?q?refactor:=20(#64)=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=A1=B0=EA=B1=B4=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 25b8cdcb4..0de36b7ff 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -101,7 +101,6 @@ public List fetchRecommendations(ClovaRecommendatio private boolean containsInvalidKeyword(List clovaResponses) { return clovaResponses.stream().anyMatch(clovaResponse -> clovaResponse.getKeywordCategory() == null - || clovaResponse.getKeywordCategory().toString().isEmpty() || !isValidKeywordCategory(clovaResponse.getKeywordCategory())); } @@ -110,10 +109,12 @@ private boolean isValidKeywordCategory(Keyword.Category keywordCategory) { } private void validateClovaRecommendationRequestKeyword(ClovaRecommendationRequest clovaRecommendationRequest) { - if (clovaRecommendationRequest.activityType().equals(Type.ONLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("NATURE")) { + if (clovaRecommendationRequest.activityType().equals(Type.ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.NATURE) + ) { throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); } - if (clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.toString(clovaRecommendationRequest.keywords()).contains("SOCIAL")) { + if (clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.SOCIAL) + ) { throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); } } From c6edda44ca268333448396501f1f01e072f2c64d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 12:40:34 +0900 Subject: [PATCH 232/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tRequest=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectRequest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java new file mode 100644 index 000000000..aaed25060 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java @@ -0,0 +1,39 @@ +package spring.backend.activity.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; + +import java.util.Set; + +public record QuickStartActivitySelectRequest( + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") + Type type, + + @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") + @Min(value = 10, message = "자투리 시간은 최소 10이어야 합니다.") + @Max(value = 300, message = "자투리 시간은 최대 300이어야 합니다.") + @Schema(description = "자투리 시간", example = "300") + Integer spareTime, + + @NotNull(message = "키워드는 최대 5가지 입력 가능하며, 키워드를 선택하지 않은 경우 빈 배열을 보내주세요") + @Schema(description = "키워드(SELF_DEVELOPMENT, HEALTH, NATURE, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", + example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}, {\"category\": \"HEALTH\", \"image\": \"image_url\"}]") + Set keywords, + + @NotNull(message = "타이틀은 필수 입력 항목입니다.") + @Schema(description = "타이틀", example = "카페에서 커피 마시며 책 읽기") + String title, + + @NotNull(message = "Content는 필수 입력 항목입니다.") + @Schema(description = "내용", example = "조용한 카페에서 좋아하는 책을 읽으며 여유로운 시간을 즐길 수 있습니다.") + String content, + + @Schema(description = "장소", example = "서울시 강남구 역삼동") + String location +) { +} From 9916d77d43f4607c34847dff92339ca9414e811b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 12:42:03 +0900 Subject: [PATCH 233/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tController=EC=99=80=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectController.java | 28 +++++++++++++++++++ .../QuickStartActivitySelectSwagger.java | 26 +++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java new file mode 100644 index 000000000..9ff3a8810 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java @@ -0,0 +1,28 @@ +package spring.backend.activity.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.QuickStartActivitySelectService; +import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.presentation.swagger.QuickStartActivitySelectSwagger; +import spring.backend.core.configuration.argumentresolver.LoginMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class QuickStartActivitySelectController implements QuickStartActivitySelectSwagger { + + private final QuickStartActivitySelectService quickStartActivitySelectService; + + @Override + @Authorization + @PostMapping("/v1/quick-starts/{quickStartId}/activities") + public Long quickStartUserActivitySelect(@LoginMember Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest) { + // TODO Auto-generated method stub + Long result = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); + return result; + } +} diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java new file mode 100644 index 000000000..3509373a7 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -0,0 +1,26 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PathVariable; +import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "QuickStartActivitySelect", description = "빠른 시작 활동 선택") +public interface QuickStartActivitySelectSwagger { + + @Operation( + summary = "빠른 시작 활동 선택 API", + description = "빠른 시작 활동을 선택합니다.", + operationId = "/v1/quick-starts/{quickStartId}/activities" + ) + @ApiErrorCode({ + GlobalErrorCode.class, ActivityErrorCode.class, QuickStartErrorCode.class + }) + Long quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); +} From b0e8188b080a69338d0dfe4120b6513a74afc251 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 1 Nov 2024 12:42:16 +0900 Subject: [PATCH 234/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tService=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectService.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java new file mode 100644 index 000000000..0df168c9b --- /dev/null +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -0,0 +1,21 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.member.domain.entity.Member; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional +public class QuickStartActivitySelectService { + private final ActivityRepository activityRepository; + + public Long quickStartUserActivitySelect(Member member , Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest + ) {} + +} From b8940227274f2f42c7fef5925f0a374ba0dd0086 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 21:43:14 +0900 Subject: [PATCH 235/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tSwagger=EB=A5=BC=20=EB=A7=8C=EB=93=A4=EA=B3=A0=20Controller?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectController.java | 12 ++++++++---- .../swagger/QuickStartActivitySelectSwagger.java | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java index 9ff3a8810..a4abd9e71 100644 --- a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java @@ -1,14 +1,19 @@ package spring.backend.activity.presentation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.QuickStartActivitySelectService; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; import spring.backend.activity.presentation.swagger.QuickStartActivitySelectSwagger; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @RestController @@ -20,9 +25,8 @@ public class QuickStartActivitySelectController implements QuickStartActivitySel @Override @Authorization @PostMapping("/v1/quick-starts/{quickStartId}/activities") - public Long quickStartUserActivitySelect(@LoginMember Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest) { - // TODO Auto-generated method stub - Long result = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); - return result; + public ResponseEntity> quickStartUserActivitySelect(@AuthorizedMember Member member, @PathVariable Long quickStartId, @Valid @RequestBody QuickStartActivitySelectRequest quickStartActivitySelectRequest) { + Long savedActivityIdCreatedByQuickStart = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); + return ResponseEntity.ok(new RestResponse<>(savedActivityIdCreatedByQuickStart)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java index 3509373a7..a3950c80d 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -3,15 +3,17 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -@Tag(name = "QuickStartActivitySelect", description = "빠른 시작 활동 선택") +@Tag(name = "Activity", description = "활동") public interface QuickStartActivitySelectSwagger { @Operation( @@ -22,5 +24,5 @@ public interface QuickStartActivitySelectSwagger { @ApiErrorCode({ GlobalErrorCode.class, ActivityErrorCode.class, QuickStartErrorCode.class }) - Long quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); + ResponseEntity> quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); } From 1a82e6edf0f1bf80487078f8864111aed49e7748 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 21:44:20 +0900 Subject: [PATCH 236/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tRequest=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/QuickStartActivitySelectRequest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java b/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java index aaed25060..feaa609f5 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java @@ -7,8 +7,6 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -import java.util.Set; - public record QuickStartActivitySelectRequest( @NotNull(message = "활동 유형은 필수 입력 항목입니다.") @Schema(description = "활동 유형 (ONLINE, OFFLINE, ONLINE_AND_OFFLINE)", example = "ONLINE") @@ -20,16 +18,15 @@ public record QuickStartActivitySelectRequest( @Schema(description = "자투리 시간", example = "300") Integer spareTime, - @NotNull(message = "키워드는 최대 5가지 입력 가능하며, 키워드를 선택하지 않은 경우 빈 배열을 보내주세요") - @Schema(description = "키워드(SELF_DEVELOPMENT, HEALTH, NATURE, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", - example = "[{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"image_url\"}, {\"category\": \"HEALTH\", \"image\": \"image_url\"}]") - Set keywords, + @NotNull(message = "키워드는 필수 입력 항목입니다.") + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") + Keyword keyword, @NotNull(message = "타이틀은 필수 입력 항목입니다.") @Schema(description = "타이틀", example = "카페에서 커피 마시며 책 읽기") String title, - @NotNull(message = "Content는 필수 입력 항목입니다.") + @NotNull(message = "내용은 필수 입력 항목입니다.") @Schema(description = "내용", example = "조용한 카페에서 좋아하는 책을 읽으며 여유로운 시간을 즐길 수 있습니다.") String content, From 18774bffb61405155ded4f96ec10b057496e1de3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 21:44:39 +0900 Subject: [PATCH 237/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tService=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectService.java | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java index 0df168c9b..352dec1b2 100644 --- a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -4,8 +4,12 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.member.domain.entity.Member; @Service @@ -14,8 +18,28 @@ @Transactional public class QuickStartActivitySelectService { private final ActivityRepository activityRepository; + private final QuickStartRepository quickStartRepository; - public Long quickStartUserActivitySelect(Member member , Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest - ) {} + public Long quickStartUserActivitySelect(Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest + ) { + validateQuickStart(quickStartId); + validateRequest(quickStartActivitySelectRequest); + Activity activity = Activity.create(member.getId(), quickStartId, quickStartActivitySelectRequest.spareTime(), quickStartActivitySelectRequest.type(), quickStartActivitySelectRequest.keyword(), quickStartActivitySelectRequest.title(), quickStartActivitySelectRequest.content(), quickStartActivitySelectRequest.location()); + Activity savedActivity = activityRepository.save(activity); + return savedActivity.getId(); + } + private void validateQuickStart(Long quickStartId) { + if (quickStartRepository.findById(quickStartId) == null) { + log.error("[QuickStartActivitySelectRequest] Invalid quickStartId."); + throw QuickStartErrorCode.NOT_EXIST_QUICK_START.toException(); + } + } + + private void validateRequest(QuickStartActivitySelectRequest quickStartActivitySelectRequest) { + if (quickStartActivitySelectRequest == null) { + log.error("[QuickStartActivitySelectRequest] Invalid request."); + throw ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.toException(); + } + } } From 2b96707941f68335ec899b78a40fc2396a84b252 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 3 Nov 2024 22:28:11 +0900 Subject: [PATCH 238/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?tServiceTest=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectServiceTest.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java diff --git a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java new file mode 100644 index 000000000..39708099c --- /dev/null +++ b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java @@ -0,0 +1,138 @@ +package spring.backend.activity.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; +import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Role; + +import java.time.LocalTime; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class QuickStartActivitySelectServiceTest { + @InjectMocks + private QuickStartActivitySelectService quickStartActivitySelectService; + + @Mock + private ActivityRepository activityRepository; + + @Mock + private QuickStartRepository quickStartRepository; + + private Member member; + private QuickStartActivitySelectRequest quickStartActivitySelectRequest; + private final Long quickStartId = 1L; + + @BeforeEach + public void setUp() { + member = Member.builder() + .role(Role.MEMBER) + .build(); + + Keyword keyword = Keyword.create(Keyword.Category.CULTURE_ART, "example-image1.png"); + + quickStartActivitySelectRequest = new QuickStartActivitySelectRequest( + Type.OFFLINE, + 150, + keyword, + "title", + "content", + "location" + ); + } + + @DisplayName("QuickStart가 존재하지 않는 경우 예외가 발생한다") + @Test + public void throwsExceptionWhenUserActivitySelectRequestIsNull() { + // given & when + when(quickStartRepository.findById(anyLong())).thenReturn(null); + DomainException ex = assertThrows(DomainException.class, () -> + quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest) + ); + // then + assertEquals(QuickStartErrorCode.NOT_EXIST_QUICK_START.getMessage(), ex.getMessage()); + } + + @DisplayName("quickStartActivitySelectRequest가 null인 경우 예외를 반환한다.") + @Test + public void throwsExceptionWhenQuickStartActivitySelectRequestIsNull() { + QuickStart quickStart = QuickStart.create( + UUID.randomUUID(), + "name", + LocalTime.now(), + 150, + Type.ONLINE + ); + when(quickStartRepository.findById(quickStartId)).thenReturn(quickStart); + + // when + DomainException ex = assertThrows(DomainException.class, () -> + quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, null) + ); + + // then + assertEquals(ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.getMessage(), ex.getMessage()); + } + + @DisplayName("빠른시작 활동 선택에 문제가 없는 경우, 저장된 활동의 ID를 반환한다.") + @Test + public void returnSavedActivityIdWhenNothingWrong() { + // when + QuickStart quickStart = QuickStart.create( + UUID.randomUUID(), + "name", + LocalTime.now(), + 150, + Type.ONLINE + ); + when(quickStartRepository.findById(quickStartId)).thenReturn(quickStart); + Activity activity = Activity.create(member.getId(), quickStartId, quickStartActivitySelectRequest.spareTime(), quickStartActivitySelectRequest.type(), quickStartActivitySelectRequest.keyword(), quickStartActivitySelectRequest.title(), quickStartActivitySelectRequest.content(), quickStartActivitySelectRequest.location()); + when(activityRepository.save(any(Activity.class))).thenReturn(activity); + + // then + Long savedActivityId = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); + + // then + assertEquals(activity.getId(), savedActivityId); + assertEquals(activity.getQuickStartId(), quickStartId); + verify(activityRepository).save(any(Activity.class)); + } + +// @DisplayName("유효한 활동 선택인 경우 저장된 ID를 반환한다") +// @Test +// public void returnsSavedActivityIdWhenValidActivitySelection() { +// // when +// Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); +// when(activityRepository.save(any(Activity.class))).thenReturn(activity); +// +// // then +// Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); +// +// // then +// assertEquals(activity.getId(), savedActivityId); +// verify(activityRepository).save(any(Activity.class)); +// } + +} From d93378554677c82e945eb63d19bd98cbc09a22e1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:35:42 +0900 Subject: [PATCH 239/478] =?UTF-8?q?feat:=20(#65)=20=EB=B9=A0=EB=A5=B8?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=ED=99=9C=EB=8F=99=EC=84=A0=ED=83=9D=EC=9D=98?= =?UTF-8?q?=20response=20dto=EB=A5=BC=20=EB=A7=8C=EB=93=A0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectResponse.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java b/src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java new file mode 100644 index 000000000..6044b7cf0 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java @@ -0,0 +1,16 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.domain.value.Keyword; + +public record QuickStartActivitySelectResponse( + @Schema(description = "활동 ID", example = "1") + Long id, + + @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") + String title, + + @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") + Keyword keyword +) { +} From d1574c2884f39ce0b9a9029db6de83e05aed45df Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 7 Nov 2024 13:36:49 +0900 Subject: [PATCH 240/478] =?UTF-8?q?feat:=20(#65)=20QuickStartActivitySelec?= =?UTF-8?q?t=EB=8A=94=20QuickStartActivityResponse=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectService.java | 5 ++-- .../QuickStartActivitySelectController.java | 7 +++--- .../QuickStartActivitySelectSwagger.java | 3 ++- .../QuickStartActivitySelectServiceTest.java | 25 +++++-------------- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java index 352dec1b2..a83aba2ad 100644 --- a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -8,6 +8,7 @@ import spring.backend.activity.domain.repository.ActivityRepository; import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.member.domain.entity.Member; @@ -20,13 +21,13 @@ public class QuickStartActivitySelectService { private final ActivityRepository activityRepository; private final QuickStartRepository quickStartRepository; - public Long quickStartUserActivitySelect(Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest + public QuickStartActivitySelectResponse quickStartUserActivitySelect(Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest ) { validateQuickStart(quickStartId); validateRequest(quickStartActivitySelectRequest); Activity activity = Activity.create(member.getId(), quickStartId, quickStartActivitySelectRequest.spareTime(), quickStartActivitySelectRequest.type(), quickStartActivitySelectRequest.keyword(), quickStartActivitySelectRequest.title(), quickStartActivitySelectRequest.content(), quickStartActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); - return savedActivity.getId(); + return new QuickStartActivitySelectResponse(savedActivity.getId(), savedActivity.getTitle(), savedActivity.getKeyword()); } private void validateQuickStart(Long quickStartId) { diff --git a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java index a4abd9e71..cc44dd2ee 100644 --- a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.QuickStartActivitySelectService; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.presentation.swagger.QuickStartActivitySelectSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.argumentresolver.LoginMember; @@ -25,8 +26,8 @@ public class QuickStartActivitySelectController implements QuickStartActivitySel @Override @Authorization @PostMapping("/v1/quick-starts/{quickStartId}/activities") - public ResponseEntity> quickStartUserActivitySelect(@AuthorizedMember Member member, @PathVariable Long quickStartId, @Valid @RequestBody QuickStartActivitySelectRequest quickStartActivitySelectRequest) { - Long savedActivityIdCreatedByQuickStart = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); - return ResponseEntity.ok(new RestResponse<>(savedActivityIdCreatedByQuickStart)); + public ResponseEntity> quickStartUserActivitySelect(@AuthorizedMember Member member, @PathVariable Long quickStartId, @Valid @RequestBody QuickStartActivitySelectRequest quickStartActivitySelectRequest) { + QuickStartActivitySelectResponse savedActivityIdCreatedByQuickStartResponse = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); + return ResponseEntity.ok(new RestResponse<>(savedActivityIdCreatedByQuickStartResponse)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java index a3950c80d..7edb2f239 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -6,6 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; @@ -24,5 +25,5 @@ public interface QuickStartActivitySelectSwagger { @ApiErrorCode({ GlobalErrorCode.class, ActivityErrorCode.class, QuickStartErrorCode.class }) - ResponseEntity> quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); + ResponseEntity> quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); } diff --git a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java index 39708099c..e011603f5 100644 --- a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java @@ -14,6 +14,7 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.core.exception.DomainException; @@ -96,7 +97,7 @@ public void throwsExceptionWhenQuickStartActivitySelectRequestIsNull() { assertEquals(ActivityErrorCode.NOT_EXIST_ACTIVITY_CONDITION.getMessage(), ex.getMessage()); } - @DisplayName("빠른시작 활동 선택에 문제가 없는 경우, 저장된 활동의 ID를 반환한다.") + @DisplayName("빠른시작 활동 선택에 문제가 없는 경우, 저장된 활동의 QuickStartActivitySelectResponse를 반환한다.") @Test public void returnSavedActivityIdWhenNothingWrong() { // when @@ -112,27 +113,13 @@ public void returnSavedActivityIdWhenNothingWrong() { when(activityRepository.save(any(Activity.class))).thenReturn(activity); // then - Long savedActivityId = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); + QuickStartActivitySelectResponse quickStartActivitySelectResponse = quickStartActivitySelectService.quickStartUserActivitySelect(member, quickStartId, quickStartActivitySelectRequest); // then - assertEquals(activity.getId(), savedActivityId); assertEquals(activity.getQuickStartId(), quickStartId); + assertEquals(activity.getId(), quickStartActivitySelectResponse.id()); + assertEquals(activity.getTitle(), quickStartActivitySelectResponse.title()); + assertEquals(activity.getKeyword(), quickStartActivitySelectResponse.keyword()); verify(activityRepository).save(any(Activity.class)); } - -// @DisplayName("유효한 활동 선택인 경우 저장된 ID를 반환한다") -// @Test -// public void returnsSavedActivityIdWhenValidActivitySelection() { -// // when -// Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); -// when(activityRepository.save(any(Activity.class))).thenReturn(activity); -// -// // then -// Long savedActivityId = userActivitySelectService.userActivitySelect(member, userActivitySelectRequest); -// -// // then -// assertEquals(activity.getId(), savedActivityId); -// verify(activityRepository).save(any(Activity.class)); -// } - } From 0207071f4103f8b744ecbb84989fa2eb23c54304 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 8 Nov 2024 01:44:47 +0900 Subject: [PATCH 241/478] =?UTF-8?q?fix:=20(#64)=20QuickStartActivitySelect?= =?UTF-8?q?Swagger=EC=9D=98=20=EC=A4=91=EB=B3=B5=20PathVariable=EC=9D=84?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/swagger/QuickStartActivitySelectSwagger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java index 7edb2f239..c95a6301a 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -25,5 +25,5 @@ public interface QuickStartActivitySelectSwagger { @ApiErrorCode({ GlobalErrorCode.class, ActivityErrorCode.class, QuickStartErrorCode.class }) - ResponseEntity> quickStartUserActivitySelect(@Parameter(hidden = true) Member member, @PathVariable Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); + ResponseEntity> quickStartUserActivitySelect(@Parameter(hidden = true) Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest); } From e791e45ba7c87d410c9ccdc56c9145edcba2e54d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 10 Nov 2024 18:52:27 +0900 Subject: [PATCH 242/478] =?UTF-8?q?feat:=20(#84)=20Youtube=20API=EB=A5=BC?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/link/LinkWebClient.java | 6 ++ .../link/youtube/YoutubeWebClient.java | 62 +++++++++++++++++++ .../youtube/dto/response/YoutubeResponse.java | 55 ++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/link/LinkWebClient.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/link/youtube/YoutubeWebClient.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/link/youtube/dto/response/YoutubeResponse.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/link/LinkWebClient.java b/src/main/java/spring/backend/recommendation/infrastructure/link/LinkWebClient.java new file mode 100644 index 000000000..0d343e758 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/link/LinkWebClient.java @@ -0,0 +1,6 @@ +package spring.backend.recommendation.infrastructure.link; + +public interface LinkWebClient { + + T search(String query); +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/YoutubeWebClient.java b/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/YoutubeWebClient.java new file mode 100644 index 000000000..600ea51c3 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/YoutubeWebClient.java @@ -0,0 +1,62 @@ +package spring.backend.recommendation.infrastructure.link.youtube; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; +import org.springframework.web.util.UriComponentsBuilder; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.recommendation.infrastructure.link.LinkWebClient; +import spring.backend.recommendation.infrastructure.link.youtube.dto.response.YoutubeResponse; + +import java.net.URI; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class YoutubeWebClient implements LinkWebClient { + + @Value("${youtube.api-key}") + private String apiKey; + + @Value("${youtube.search-url}") + private String searchUrl; + + @Override + public YoutubeResponse search(String query) { + try { + return WebClient.create() + .get() + .uri(buildSearchUrl(query)) + .retrieve() + .bodyToMono(YoutubeResponse.class) + .block(); + } catch (WebClientException e) { + log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e); + throw GlobalErrorCode.WEB_CLIENT_ERROR.toException(); + } catch (Exception e) { + log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } + + private URI buildSearchUrl(String query) { + return UriComponentsBuilder.fromUriString(searchUrl) + .queryParams(createSearchRequestParams(query)) + .build() + .toUri(); + } + + private MultiValueMap createSearchRequestParams(String query) { + final String part = "snippet"; + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", apiKey); + params.add("part", part); + params.add("q", query); + return params; + } +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/dto/response/YoutubeResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/dto/response/YoutubeResponse.java new file mode 100644 index 000000000..1d5ffde23 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/link/youtube/dto/response/YoutubeResponse.java @@ -0,0 +1,55 @@ +package spring.backend.recommendation.infrastructure.link.youtube.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +public record YoutubeResponse( + String kind, + String etag, + String nextPageToken, + String regionCode, + PageInfo pageInfo, + List items +) { + public record PageInfo( + int totalResults, + int resultsPerPage + ) {} + + public record YoutubeSearchItem( + String kind, + String etag, + Id id, + Snippet snippet + ) { + public record Id( + String kind, + String videoId, + String channelId, + String playlistId + ) {} + + public record Snippet( + LocalDateTime publishedAt, + String channelId, + String title, + String description, + Thumbnails thumbnails, + String channelTitle, + String liveBroadcastContent, + LocalDateTime publishTime + ) { + public record Thumbnails( + Thumbnail defaultThumbnail, + Thumbnail medium, + Thumbnail high + ) { + public record Thumbnail( + String url, + int width, + int height + ) {} + } + } + } +} From e603172c171ec251ed2dcc72ff5ded75d3277e62 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 10 Nov 2024 00:36:38 +0900 Subject: [PATCH 243/478] =?UTF-8?q?feat:=20(#80)=20MapService=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/recommendation/application/MapService.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/application/MapService.java diff --git a/src/main/java/spring/backend/recommendation/application/MapService.java b/src/main/java/spring/backend/recommendation/application/MapService.java new file mode 100644 index 000000000..ec32d1848 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/application/MapService.java @@ -0,0 +1,5 @@ +package spring.backend.recommendation.application; + +public interface MapService { + void search(String query); +} From a762af91d963e9b010e7b7a8292401417e5994a8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 10 Nov 2024 00:54:11 +0900 Subject: [PATCH 244/478] =?UTF-8?q?feat:=20(#80)=20NaverMapErrorCode?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../naver/exception/NaverMapErrorCode.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java new file mode 100644 index 000000000..78415a542 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java @@ -0,0 +1,24 @@ +package spring.backend.recommendation.infrastructure.map.naver.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum NaverMapErrorCode implements BaseErrorCode { + API_REQUEST_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "API 요청과 응답이 실패했습니다."), + FAILED_TO_CONNECT_API(HttpStatus.INTERNAL_SERVER_ERROR, "API 연결에 실패했습니다."), + FAILED_TO_READ_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "API 응답을 읽는데 실패했습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From 0c691147245404497da67bb068511e3ae980a922 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 10 Nov 2024 00:54:28 +0900 Subject: [PATCH 245/478] =?UTF-8?q?feat:=20(#80)=20NaverMapService?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../naver/application/NaverMapService.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java new file mode 100644 index 000000000..8a8ea660f --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java @@ -0,0 +1,103 @@ +package spring.backend.recommendation.infrastructure.map.naver.application; + +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import spring.backend.recommendation.application.MapService; +import spring.backend.recommendation.infrastructure.map.naver.exception.NaverMapErrorCode; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +@Service +@Log4j2 +public class NaverMapService implements MapService { + @Value("${naver.client-id}") + private String clientId; + + @Value("${naver.client-secret}") + private String clientSecret; + + @Value("${naver.map.base-uri}") + private String baseUri; + + + @Override + public void search(String query) { + String encodedQuery = encodeSearchQuery(query); + String apiUrl = baseUri + "?query=" + encodedQuery; + Map requestHeaders = createHeaders(); + + String responseBody = fetchResponse(apiUrl, requestHeaders); + } + + private Map createHeaders() { + Map headers = new HashMap<>(); + headers.put("X-Naver-Client-Id", clientId); + headers.put("X-Naver-Client-Secret", clientSecret); + return headers; + } + + private String encodeSearchQuery(String query) { + return URLEncoder.encode(query, StandardCharsets.UTF_8); + } + + private String fetchResponse(String apiUrl, Map requestHeaders) { + HttpURLConnection connection = createConnection(apiUrl); + try { + connection.setRequestMethod("GET"); + requestHeaders.forEach(connection::setRequestProperty); + + int responseCode = connection.getResponseCode(); + return responseCode == HttpURLConnection.HTTP_OK + ? readStream(connection.getInputStream()) + : readStream(connection.getErrorStream()); + } catch (IOException e) { + log.error( + "API 요청과 응답이 실패했습니다. - 에러 메시지: {}", + e.getMessage(), + e + ); + throw NaverMapErrorCode.API_REQUEST_FAILED.toException(); + } finally { + connection.disconnect(); + } + } + + private HttpURLConnection createConnection(String apiUrl) { + try { + URL url = new URL(apiUrl); + return (HttpURLConnection) url.openConnection(); + } catch (IOException e) { + log.error( + "API 연결에 실패했습니다. - 에러 메시지: {}", + e.getMessage(), + e + ); + throw NaverMapErrorCode.FAILED_TO_CONNECT_API.toException(); + } + } + + private String readStream(InputStream stream) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } catch (IOException e) { + log.error( + "API 응답을 읽는데 실패했습니다. - 에러 메시지: {}", + e.getMessage(), + e + ); + throw NaverMapErrorCode.FAILED_TO_READ_RESPONSE.toException(); + } + } +} From a86202b4fb4e0591cf2a1d188d8c02c1eb034702 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 11 Nov 2024 16:01:37 +0900 Subject: [PATCH 246/478] =?UTF-8?q?feat:=20(#80)=20NaverMapResponse=20dto?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../naver/dto/response/NaverMapResponse.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/naver/dto/response/NaverMapResponse.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/dto/response/NaverMapResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/dto/response/NaverMapResponse.java new file mode 100644 index 000000000..4b1de5748 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/dto/response/NaverMapResponse.java @@ -0,0 +1,24 @@ +package spring.backend.recommendation.infrastructure.map.naver.dto.response; + +import java.util.List; + +public record NaverMapResponse( + String lastBuildDate, + int total, + int start, + int display, + List items +) { + public record NaverMapSearchItem( + String title, + String link, + String category, + String description, + String telephone, + String address, + String roadAddress, + double mapx, + double mapy + ) { + } +} From 0a91f7cfaf751cf88e4947aa5d505c0ae6aaaddd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 11 Nov 2024 16:01:56 +0900 Subject: [PATCH 247/478] =?UTF-8?q?feat:=20(#80)=20NaverMapErrorCode?= =?UTF-8?q?=EC=97=90=20=ED=8C=8C=EC=8B=B1=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/map/naver/exception/NaverMapErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java index 78415a542..927fc00e9 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/exception/NaverMapErrorCode.java @@ -11,7 +11,8 @@ public enum NaverMapErrorCode implements BaseErrorCode { API_REQUEST_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "API 요청과 응답이 실패했습니다."), FAILED_TO_CONNECT_API(HttpStatus.INTERNAL_SERVER_ERROR, "API 연결에 실패했습니다."), - FAILED_TO_READ_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "API 응답을 읽는데 실패했습니다."); + FAILED_TO_READ_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "API 응답을 읽는데 실패했습니다."), + FAILED_TO_PARSE_RESPONSE(HttpStatus.INTERNAL_SERVER_ERROR, "응답을 파싱하는데 실패했습니다."); private final HttpStatus httpStatus; From 6876c30009fc550524c3fb1500ec7eab85884f4d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 11 Nov 2024 16:02:27 +0900 Subject: [PATCH 248/478] =?UTF-8?q?feat:=20(#80)=20NaverMapResponse?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=98=ED=99=98=EA=B0=92=EC=9D=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recommendation/application/MapService.java | 4 +++- .../map/naver/application/NaverMapService.java | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/MapService.java b/src/main/java/spring/backend/recommendation/application/MapService.java index ec32d1848..43b856dbd 100644 --- a/src/main/java/spring/backend/recommendation/application/MapService.java +++ b/src/main/java/spring/backend/recommendation/application/MapService.java @@ -1,5 +1,7 @@ package spring.backend.recommendation.application; +import spring.backend.recommendation.infrastructure.map.naver.dto.response.NaverMapResponse; + public interface MapService { - void search(String query); + NaverMapResponse search(String query); } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java index 8a8ea660f..1d673906d 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java @@ -1,9 +1,11 @@ package spring.backend.recommendation.infrastructure.map.naver.application; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import spring.backend.recommendation.application.MapService; +import spring.backend.recommendation.infrastructure.map.naver.dto.response.NaverMapResponse; import spring.backend.recommendation.infrastructure.map.naver.exception.NaverMapErrorCode; import java.io.*; @@ -26,14 +28,16 @@ public class NaverMapService implements MapService { @Value("${naver.map.base-uri}") private String baseUri; + private final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void search(String query) { + public NaverMapResponse search(String query) { String encodedQuery = encodeSearchQuery(query); String apiUrl = baseUri + "?query=" + encodedQuery; Map requestHeaders = createHeaders(); String responseBody = fetchResponse(apiUrl, requestHeaders); + return parseResponse(responseBody); } private Map createHeaders() { @@ -100,4 +104,13 @@ private String readStream(InputStream stream) { throw NaverMapErrorCode.FAILED_TO_READ_RESPONSE.toException(); } } + + private NaverMapResponse parseResponse(String responseBody) { + try { + return objectMapper.readValue(responseBody, NaverMapResponse.class); + } catch (IOException e) { + log.error("응답을 파싱하는데 실패했습니다. - 에러 메시지: {}", e.getMessage(), e); + throw NaverMapErrorCode.FAILED_TO_PARSE_RESPONSE.toException(); + } + } } From 869af9dd1c8777592593caec6903a46ac4c5e965 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 12 Nov 2024 15:50:12 +0900 Subject: [PATCH 249/478] =?UTF-8?q?refactor:=20(#80)=20MapService=20?= =?UTF-8?q?=EB=A5=BC=20PlaceInfoProvider=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{MapService.java => PlaceInfoProvider.java} | 2 +- ...verMapService.java => NaverPlaceInfoProvider.java} | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) rename src/main/java/spring/backend/recommendation/application/{MapService.java => PlaceInfoProvider.java} (83%) rename src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/{NaverMapService.java => NaverPlaceInfoProvider.java} (93%) diff --git a/src/main/java/spring/backend/recommendation/application/MapService.java b/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java similarity index 83% rename from src/main/java/spring/backend/recommendation/application/MapService.java rename to src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java index 43b856dbd..c651e473e 100644 --- a/src/main/java/spring/backend/recommendation/application/MapService.java +++ b/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java @@ -2,6 +2,6 @@ import spring.backend.recommendation.infrastructure.map.naver.dto.response.NaverMapResponse; -public interface MapService { +public interface PlaceInfoProvider { NaverMapResponse search(String query); } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java similarity index 93% rename from src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java rename to src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java index 1d673906d..672578d19 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverMapService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java @@ -1,10 +1,12 @@ package spring.backend.recommendation.infrastructure.map.naver.application; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -import spring.backend.recommendation.application.MapService; +import spring.backend.recommendation.application.PlaceInfoProvider; import spring.backend.recommendation.infrastructure.map.naver.dto.response.NaverMapResponse; import spring.backend.recommendation.infrastructure.map.naver.exception.NaverMapErrorCode; @@ -16,9 +18,10 @@ import java.util.HashMap; import java.util.Map; -@Service +@Component @Log4j2 -public class NaverMapService implements MapService { +@RequiredArgsConstructor +public class NaverPlaceInfoProvider implements PlaceInfoProvider { @Value("${naver.client-id}") private String clientId; @@ -28,7 +31,7 @@ public class NaverMapService implements MapService { @Value("${naver.map.base-uri}") private String baseUri; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; @Override public NaverMapResponse search(String query) { From 20d5f4a3c53884e1d5cbd428ca969c24e4b80dac Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 13 Nov 2024 02:45:15 +0900 Subject: [PATCH 250/478] =?UTF-8?q?chore:=20(#88)=20email=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 8b48d4c4a..90fa430c5 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ dependencies { // Netty implementation "io.netty:netty-resolver-dns-native-macos:4.1.113.Final:osx-aarch_64" + // Email + implementation 'org.springframework.boot:spring-boot-starter-mail' + } tasks.named('test') { From 5b627c2f2b69ade57f93fd71aeeb93cd5ba2593b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 13 Nov 2024 02:45:35 +0900 Subject: [PATCH 251/478] =?UTF-8?q?chore:=20(#88)=20MailConfiguration?= =?UTF-8?q?=EC=9D=84=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../email/MailConfiguration.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/email/MailConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/email/MailConfiguration.java b/src/main/java/spring/backend/core/configuration/email/MailConfiguration.java new file mode 100644 index 000000000..74d5fe0a9 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/email/MailConfiguration.java @@ -0,0 +1,50 @@ +package spring.backend.core.configuration.email; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +@RequiredArgsConstructor +public class MailConfiguration { + + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private String smtpAuth; + + @Value("${spring.mail.properties.mail.debug}") + private String debug; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", smtpAuth); + props.put("mail.smtp.starttls.enable", smtpAuth); + props.put("mail.debug", debug); + + return mailSender; + } +} From e218449b3485a335f8c9084ea1d47bc3899fa758 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 13 Nov 2024 02:45:59 +0900 Subject: [PATCH 252/478] =?UTF-8?q?feat:=20(#88)=20Email=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EC=9D=84=20=EC=9C=84=ED=95=9C=20Util=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dto/request/SendEmailRequest.java | 8 +++++ .../spring/backend/core/util/EmailUtil.java | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/main/java/spring/backend/core/dto/request/SendEmailRequest.java create mode 100644 src/main/java/spring/backend/core/util/EmailUtil.java diff --git a/src/main/java/spring/backend/core/dto/request/SendEmailRequest.java b/src/main/java/spring/backend/core/dto/request/SendEmailRequest.java new file mode 100644 index 000000000..329b2cfda --- /dev/null +++ b/src/main/java/spring/backend/core/dto/request/SendEmailRequest.java @@ -0,0 +1,8 @@ +package spring.backend.core.dto.request; + +public record SendEmailRequest( + String to, + String subject, + String text +) { +} diff --git a/src/main/java/spring/backend/core/util/EmailUtil.java b/src/main/java/spring/backend/core/util/EmailUtil.java new file mode 100644 index 000000000..ca2bf24dc --- /dev/null +++ b/src/main/java/spring/backend/core/util/EmailUtil.java @@ -0,0 +1,30 @@ +package spring.backend.core.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; +import spring.backend.core.dto.request.SendEmailRequest; + +@Component +public class EmailUtil { + @Value("${spring.mail.sender}") + private String sender; + + private final JavaMailSender mailSender; + + @Autowired + public EmailUtil(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + public void send(SendEmailRequest sendEmailRequest) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(sender); + message.setTo(sendEmailRequest.to()); + message.setSubject(sendEmailRequest.subject()); + message.setText(sendEmailRequest.text()); + mailSender.send(message); + } +} From 854b10e2143042656955f89b94882d605baa5af2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 13 Nov 2024 17:38:47 +0900 Subject: [PATCH 253/478] =?UTF-8?q?feat:=20(#88)=20MailErrorCode=EB=A5=BC?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/email/exception/MailErrorCode.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java diff --git a/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java b/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java new file mode 100644 index 000000000..9233300b3 --- /dev/null +++ b/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java @@ -0,0 +1,27 @@ +package spring.backend.core.util.email.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum MailErrorCode implements BaseErrorCode { + + FAILED_TO_PARSE_MAIL(HttpStatus.BAD_REQUEST, "메일 파싱 중 오류가 발생했습니다."), + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "메일 서버 인증에 실패했습니다."), + ERROR_OCCURRED_SENDING_MAIL(HttpStatus.INTERNAL_SERVER_ERROR, "메일 전송 중 오류가 발생했습니다."), + GENERAL_MAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "메일 처리 중 예기치 않은 오류가 발생했습니다."), + INVALID_MAIL_ADDRESS(HttpStatus.BAD_REQUEST, "올바르지 않은 이메일 주소입니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From f2813f36953e3e9ad1c5b3d70a2fdc8bedf6e8df Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 13 Nov 2024 17:39:30 +0900 Subject: [PATCH 254/478] =?UTF-8?q?refactor:=20(#88)=20EmailUtil=EC=9D=98?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EA=B3=A0=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EC=99=80=20=EB=A1=9C=EA=B9=85=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/util/EmailUtil.java | 30 --------- .../backend/core/util/email/EmailUtil.java | 61 +++++++++++++++++++ .../email}/dto/request/SendEmailRequest.java | 2 +- 3 files changed, 62 insertions(+), 31 deletions(-) delete mode 100644 src/main/java/spring/backend/core/util/EmailUtil.java create mode 100644 src/main/java/spring/backend/core/util/email/EmailUtil.java rename src/main/java/spring/backend/core/{ => util/email}/dto/request/SendEmailRequest.java (66%) diff --git a/src/main/java/spring/backend/core/util/EmailUtil.java b/src/main/java/spring/backend/core/util/EmailUtil.java deleted file mode 100644 index ca2bf24dc..000000000 --- a/src/main/java/spring/backend/core/util/EmailUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package spring.backend.core.util; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.stereotype.Component; -import spring.backend.core.dto.request.SendEmailRequest; - -@Component -public class EmailUtil { - @Value("${spring.mail.sender}") - private String sender; - - private final JavaMailSender mailSender; - - @Autowired - public EmailUtil(JavaMailSender mailSender) { - this.mailSender = mailSender; - } - - public void send(SendEmailRequest sendEmailRequest) { - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(sender); - message.setTo(sendEmailRequest.to()); - message.setSubject(sendEmailRequest.subject()); - message.setText(sendEmailRequest.text()); - mailSender.send(message); - } -} diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java new file mode 100644 index 000000000..000ea07e7 --- /dev/null +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -0,0 +1,61 @@ +package spring.backend.core.util.email; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.*; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; +import spring.backend.core.util.email.dto.request.SendEmailRequest; +import spring.backend.core.util.email.exception.MailErrorCode; + +import java.util.regex.Pattern; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class EmailUtil { + @Value("${spring.mail.sender}") + private String sender; + + private static final Pattern EMAIL_PATTERN = Pattern.compile( + "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" + ); + + private final JavaMailSender mailSender; + + public void send(SendEmailRequest sendEmailRequest) { + validateEmailAddress(sendEmailRequest); + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(sender); + message.setTo(sendEmailRequest.to()); + message.setSubject(sendEmailRequest.subject()); + message.setText(sendEmailRequest.text()); + mailSender.send(message); + } catch (MailParseException e) { + log.error("[EmailUtil] Failed to parse email for recipient: {}, subject: {}. Error: {}", + sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + throw MailErrorCode.FAILED_TO_PARSE_MAIL.toException(); + } catch (MailAuthenticationException e) { + log.error("[EmailUtil] Authentication failed for email sender: {}. Error: {}", + sender, e.getMessage()); + throw MailErrorCode.AUTHENTICATION_FAILED.toException(); + } catch (MailSendException e) { + log.error("[EmailUtil] Error occurred while sending email to recipient: {}, subject: {}. Error: {}", + sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + throw MailErrorCode.ERROR_OCCURRED_SENDING_MAIL.toException(); + } catch (MailException e) { + log.error("[EmailUtil] General mail error for recipient: {}, subject: {}. Error: {}", + sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + throw MailErrorCode.GENERAL_MAIL_ERROR.toException(); + } + } + + private void validateEmailAddress(SendEmailRequest request) { + if (request.to() == null || !EMAIL_PATTERN.matcher(request.to()).matches()) { + log.error("[EmailUtil] Invalid email address format: {}", request.to()); + throw MailErrorCode.INVALID_MAIL_ADDRESS.toException(); + } + } +} diff --git a/src/main/java/spring/backend/core/dto/request/SendEmailRequest.java b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java similarity index 66% rename from src/main/java/spring/backend/core/dto/request/SendEmailRequest.java rename to src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java index 329b2cfda..d9ee2b217 100644 --- a/src/main/java/spring/backend/core/dto/request/SendEmailRequest.java +++ b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java @@ -1,4 +1,4 @@ -package spring.backend.core.dto.request; +package spring.backend.core.util.email.dto.request; public record SendEmailRequest( String to, From 9fa71b642297c9103fda002868a1f5bde3dd1da2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 12:46:01 +0900 Subject: [PATCH 255/478] =?UTF-8?q?refactor:=20(#88)=20Email=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B0=92=EC=9D=98=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/util/email/EmailUtil.java | 22 ++++++++++++++++++- .../util/email/exception/MailErrorCode.java | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index 000ea07e7..275785d48 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -25,7 +25,7 @@ public class EmailUtil { private final JavaMailSender mailSender; public void send(SendEmailRequest sendEmailRequest) { - validateEmailAddress(sendEmailRequest); + validateEmailRequest(sendEmailRequest); try { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(sender); @@ -52,10 +52,30 @@ public void send(SendEmailRequest sendEmailRequest) { } } + private void validateEmailRequest(SendEmailRequest request) { + validateEmailAddress(request); + validateEmailSubject(request); + validateEmailText(request); + } + private void validateEmailAddress(SendEmailRequest request) { if (request.to() == null || !EMAIL_PATTERN.matcher(request.to()).matches()) { log.error("[EmailUtil] Invalid email address format: {}", request.to()); throw MailErrorCode.INVALID_MAIL_ADDRESS.toException(); } } + + private void validateEmailSubject(SendEmailRequest request) { + if (request.subject() == null || request.subject().isEmpty()) { + log.error("[EmailUtil] Invalid email title: {}", request.subject()); + throw MailErrorCode.NO_MAIL_TITLE.toException(); + } + } + + private void validateEmailText(SendEmailRequest request) { + if (request.text() == null || request.text().isEmpty()) { + log.error("[EmailUtil] Invalid email subject: {}", request.subject()); + throw MailErrorCode.NO_MAIL_CONTENT.toException(); + } + } } diff --git a/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java b/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java index 9233300b3..a930c99b0 100644 --- a/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java +++ b/src/main/java/spring/backend/core/util/email/exception/MailErrorCode.java @@ -11,6 +11,8 @@ public enum MailErrorCode implements BaseErrorCode { FAILED_TO_PARSE_MAIL(HttpStatus.BAD_REQUEST, "메일 파싱 중 오류가 발생했습니다."), + NO_MAIL_CONTENT(HttpStatus.BAD_REQUEST, "메일 내용이 없습니다."), + NO_MAIL_TITLE(HttpStatus.BAD_REQUEST, "메일 제목이 없습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "메일 서버 인증에 실패했습니다."), ERROR_OCCURRED_SENDING_MAIL(HttpStatus.INTERNAL_SERVER_ERROR, "메일 전송 중 오류가 발생했습니다."), GENERAL_MAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "메일 처리 중 예기치 않은 오류가 발생했습니다."), From 72dd28546475e911b3021d3e9d963f83066ddef9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 14:04:05 +0900 Subject: [PATCH 256/478] =?UTF-8?q?feat:=20(#88)=20EmailUtilTest=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/util/EmailUtilTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/test/java/spring/backend/core/util/EmailUtilTest.java diff --git a/src/test/java/spring/backend/core/util/EmailUtilTest.java b/src/test/java/spring/backend/core/util/EmailUtilTest.java new file mode 100644 index 000000000..24787b4c2 --- /dev/null +++ b/src/test/java/spring/backend/core/util/EmailUtilTest.java @@ -0,0 +1,80 @@ +package spring.backend.core.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import spring.backend.core.exception.DomainException; +import spring.backend.core.util.email.EmailUtil; +import spring.backend.core.util.email.dto.request.SendEmailRequest; +import spring.backend.core.util.email.exception.MailErrorCode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +public class EmailUtilTest { + @Mock + private JavaMailSender javaMailSender; + + @InjectMocks + private EmailUtil emailUtil; + + @Value("${spring.mail.sender}") + private String sender; + + private SendEmailRequest sendEmailRequest; + + + @DisplayName("SendEmailRequest의 to 값이 올바르지 않은 이메일 형식의 경우 예외를 반환한다.") + @Test + void throwExceptionWhenToInRequestIsInvalid() { + // GIVEN + sendEmailRequest = new SendEmailRequest("test", "Test Subject", "Test Content"); + + // WHEN & THEN + DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "올바르지 않은 이메일 주소입니다."); + assertThat(ex.getCode()).isEqualTo(MailErrorCode.INVALID_MAIL_ADDRESS.name()); + assertThat(ex.getMessage()).isEqualTo(MailErrorCode.INVALID_MAIL_ADDRESS.getMessage()); + } + + @DisplayName("SendEmailRequest의 to 값이 null인 경우 예외를 반환한다.") + @Test + void throwExceptionWhenToInRequestIsNull() { + // GIVEN + sendEmailRequest = new SendEmailRequest(null, "Test Subject", "Test Content"); + + // WHEN & THEN + DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "올바르지 않은 이메일 주소입니다."); + assertThat(ex.getCode()).isEqualTo(MailErrorCode.INVALID_MAIL_ADDRESS.name()); + assertThat(ex.getMessage()).isEqualTo(MailErrorCode.INVALID_MAIL_ADDRESS.getMessage()); + } + + @DisplayName("SendEmailRequest의 subject가 비어있는 경우 예외를 반환한다.") + @Test + void throwExceptionWhenSubjectInRequestIsNUll() { + // GIVEN + sendEmailRequest = new SendEmailRequest("test@naver.com", "", "Test Content"); + + // WHEN & THEN + DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 제목이 없습니다."); + assertThat(ex.getCode()).isEqualTo(MailErrorCode.NO_MAIL_TITLE.name()); + assertThat(ex.getMessage()).isEqualTo(MailErrorCode.NO_MAIL_CONTENT.getMessage()); + } + + @DisplayName("SendEmailRequest의 text가 비어있는 경우 예외를 반환한다.") + @Test + void throwExceptionWhenTextInRequestIsNull() { + // GIVEN + sendEmailRequest = new SendEmailRequest("test@naver.com", "Test Subject", ""); + + // WHEN & THEN + DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 내용이 없습니다."); + assertThat(ex.getCode()).isEqualTo(MailErrorCode.NO_MAIL_CONTENT.name()); + assertThat(ex.getMessage()).isEqualTo(MailErrorCode.NO_MAIL_CONTENT.getMessage()); + } +} From 6132c6f3bb60e192c1df3a987ced69281269deb4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 09:46:29 +0900 Subject: [PATCH 257/478] =?UTF-8?q?refactor:=20(#88)=20SendEmailRequest=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/util/email/EmailUtil.java | 24 +++++++++---------- .../email/dto/request/SendEmailRequest.java | 6 ++--- .../backend/core/util/EmailUtilTest.java | 11 ++------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index 275785d48..9cdf998f1 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -29,13 +29,13 @@ public void send(SendEmailRequest sendEmailRequest) { try { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(sender); - message.setTo(sendEmailRequest.to()); - message.setSubject(sendEmailRequest.subject()); - message.setText(sendEmailRequest.text()); + message.setTo(sendEmailRequest.receiver()); + message.setSubject(sendEmailRequest.title()); + message.setText(sendEmailRequest.content()); mailSender.send(message); } catch (MailParseException e) { log.error("[EmailUtil] Failed to parse email for recipient: {}, subject: {}. Error: {}", - sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.FAILED_TO_PARSE_MAIL.toException(); } catch (MailAuthenticationException e) { log.error("[EmailUtil] Authentication failed for email sender: {}. Error: {}", @@ -43,11 +43,11 @@ public void send(SendEmailRequest sendEmailRequest) { throw MailErrorCode.AUTHENTICATION_FAILED.toException(); } catch (MailSendException e) { log.error("[EmailUtil] Error occurred while sending email to recipient: {}, subject: {}. Error: {}", - sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.ERROR_OCCURRED_SENDING_MAIL.toException(); } catch (MailException e) { log.error("[EmailUtil] General mail error for recipient: {}, subject: {}. Error: {}", - sendEmailRequest.to(), sendEmailRequest.subject(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.GENERAL_MAIL_ERROR.toException(); } } @@ -59,22 +59,22 @@ private void validateEmailRequest(SendEmailRequest request) { } private void validateEmailAddress(SendEmailRequest request) { - if (request.to() == null || !EMAIL_PATTERN.matcher(request.to()).matches()) { - log.error("[EmailUtil] Invalid email address format: {}", request.to()); + if (request.receiver() == null || !EMAIL_PATTERN.matcher(request.receiver()).matches()) { + log.error("[EmailUtil] Invalid email address format: {}", request.receiver()); throw MailErrorCode.INVALID_MAIL_ADDRESS.toException(); } } private void validateEmailSubject(SendEmailRequest request) { - if (request.subject() == null || request.subject().isEmpty()) { - log.error("[EmailUtil] Invalid email title: {}", request.subject()); + if (request.title() == null || request.title().isEmpty()) { + log.error("[EmailUtil] Invalid email title: {}", request.title()); throw MailErrorCode.NO_MAIL_TITLE.toException(); } } private void validateEmailText(SendEmailRequest request) { - if (request.text() == null || request.text().isEmpty()) { - log.error("[EmailUtil] Invalid email subject: {}", request.subject()); + if (request.content() == null || request.content().isEmpty()) { + log.error("[EmailUtil] Invalid email subject: {}", request.title()); throw MailErrorCode.NO_MAIL_CONTENT.toException(); } } diff --git a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java index d9ee2b217..691a07b75 100644 --- a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java +++ b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java @@ -1,8 +1,8 @@ package spring.backend.core.util.email.dto.request; public record SendEmailRequest( - String to, - String subject, - String text + String receiver, + String title, + String content ) { } diff --git a/src/test/java/spring/backend/core/util/EmailUtilTest.java b/src/test/java/spring/backend/core/util/EmailUtilTest.java index 24787b4c2..4ec23197e 100644 --- a/src/test/java/spring/backend/core/util/EmailUtilTest.java +++ b/src/test/java/spring/backend/core/util/EmailUtilTest.java @@ -18,18 +18,11 @@ @ExtendWith(MockitoExtension.class) public class EmailUtilTest { - @Mock - private JavaMailSender javaMailSender; - @InjectMocks private EmailUtil emailUtil; - @Value("${spring.mail.sender}") - private String sender; - private SendEmailRequest sendEmailRequest; - @DisplayName("SendEmailRequest의 to 값이 올바르지 않은 이메일 형식의 경우 예외를 반환한다.") @Test void throwExceptionWhenToInRequestIsInvalid() { @@ -56,14 +49,14 @@ void throwExceptionWhenToInRequestIsNull() { @DisplayName("SendEmailRequest의 subject가 비어있는 경우 예외를 반환한다.") @Test - void throwExceptionWhenSubjectInRequestIsNUll() { + void throwExceptionWhenSubjectInRequestIsNull() { // GIVEN sendEmailRequest = new SendEmailRequest("test@naver.com", "", "Test Content"); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 제목이 없습니다."); assertThat(ex.getCode()).isEqualTo(MailErrorCode.NO_MAIL_TITLE.name()); - assertThat(ex.getMessage()).isEqualTo(MailErrorCode.NO_MAIL_CONTENT.getMessage()); + assertThat(ex.getMessage()).isEqualTo(MailErrorCode.NO_MAIL_TITLE.getMessage()); } @DisplayName("SendEmailRequest의 text가 비어있는 경우 예외를 반환한다.") From c73523a5bc0c6148d4b15f47b46687eb5f7f72b7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 01:17:38 +0900 Subject: [PATCH 258/478] =?UTF-8?q?fix:=20(#91)=20AIRecommendationRequest?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=8F=84?= =?UTF-8?q?=EC=A4=91=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20ActivityErrorCode=EB=A1=9C=20=EC=98=AE?= =?UTF-8?q?=EA=B8=B4=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/activity/exception/ActivityErrorCode.java | 2 ++ .../infrastructure/clova/exception/ClovaErrorCode.java | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java index 53a65699f..e5756d5e5 100644 --- a/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java +++ b/src/main/java/spring/backend/activity/exception/ActivityErrorCode.java @@ -10,6 +10,8 @@ @RequiredArgsConstructor public enum ActivityErrorCode implements BaseErrorCode { + NOT_EXIST_LOCATION_WHEN_OFFLINE(HttpStatus.BAD_REQUEST, "오프라인의 경우 위치 정보가 필수입니다."), + EXIST_LOCATION_WHEN_ONLINE(HttpStatus.BAD_REQUEST, "온라인의 경우 위치 정보를 포함하지 않습니다."), ACTIVITY_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "활동을 저장하는데 실패하였습니다."), NOT_EXIST_ACTIVITY(HttpStatus.BAD_REQUEST, "활동이 존재하지 않습니다."), MEMBER_ID_MISMATCH(HttpStatus.FORBIDDEN, "활동과 멤버 ID가 일치하지 않습니다."), diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java index e4aaf3cdb..b7b731682 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -11,7 +11,6 @@ @RequiredArgsConstructor public enum ClovaErrorCode implements BaseErrorCode { - NOT_EXIST_LOCATION_WHEN_OFFLINE(HttpStatus.BAD_REQUEST, "오프라인의 경우 위치 정보가 필수입니다."), NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."), NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."), ONLINE_TYPE_CONTAIN_NATURE(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 ONLINE인 경우, NATURE(자연) 키워드를 사용할 수 없습니다."), From 44aeac6edbebc4d08247a14fc6c8282e68a8896e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 01:20:33 +0900 Subject: [PATCH 259/478] =?UTF-8?q?fix:=20(#91)=20ClovaRecommendationReque?= =?UTF-8?q?st=EB=A5=BC=20AIRecommendationRequest=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/RecommendationProvider.java | 6 +++--- ...onRequest.java => AIRecommendationRequest.java} | 14 +++++++++----- .../clova/application/ClovaService.java | 8 ++++---- .../clova/dto/request/ClovaRequest.java | 9 +++++---- .../recommendation/infrastructure/dto/Role.java | 11 +++++++++++ .../GetRecommendationsFromClovaController.java | 6 +++--- .../GetRecommendationsFromClovaSwagger.java | 5 ++--- 7 files changed, 37 insertions(+), 22 deletions(-) rename src/main/java/spring/backend/recommendation/dto/request/{ClovaRecommendationRequest.java => AIRecommendationRequest.java} (62%) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/dto/Role.java diff --git a/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java index f7adac481..e15d6ac02 100644 --- a/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java @@ -1,7 +1,7 @@ package spring.backend.recommendation.application; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; -public interface RecommendationProvider { - String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest); +public interface RecommendationProvider { + T getRecommendations(AIRecommendationRequest aiRecommendationRequest); } diff --git a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java similarity index 62% rename from src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java rename to src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java index 3e08d338a..85d123816 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/ClovaRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java @@ -1,24 +1,28 @@ package spring.backend.recommendation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.extern.log4j.Log4j2; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -public record ClovaRecommendationRequest(@NotNull(message = "자투리 시간은 필수 입력 항목입니다.") +@Log4j2 +public record AIRecommendationRequest(@NotNull(message = "자투리 시간은 필수 입력 항목입니다.") @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") @Schema(description = "자투리 시간", example = "30") Integer spareTime, - @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") @Schema(description = "활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE 중 하나를 선택합니다.)", example = "OFFLINE") Type activityType, - @NotNull(message = "키워드는 필수 입력 항목입니다.") + @NotNull(message = "키워드는 필수 입력 항목입니다.") @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") Keyword.Category[] keywords, - @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") + @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") String location) { } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java index 10ba8bf81..ddda1795f 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java @@ -7,7 +7,7 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; @@ -21,9 +21,9 @@ public class ClovaService { private final WebClient clovaStudioWebClient; - public ClovaResponse requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { + public ClovaResponse requestToClovaStudio(AIRecommendationRequest aiRecommendationRequest) { try { - ClovaRequest request = ClovaRequest.createClovaRequest(clovaRecommendationRequest); + ClovaRequest request = ClovaRequest.createClovaRequest(aiRecommendationRequest); return clovaStudioWebClient.post() .uri(apiUrl) @@ -39,4 +39,4 @@ public ClovaResponse requestToClovaStudio(ClovaRecommendationRequest clovaRecomm throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } -} \ No newline at end of file +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index 70896d863..873779614 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -2,7 +2,8 @@ import lombok.Builder; import lombok.Getter; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.infrastructure.dto.Message; import java.util.ArrayList; @@ -26,10 +27,10 @@ public class ClovaRequest { private boolean includeAiFilters; private int seed; - public static ClovaRequest createClovaRequest(ClovaRecommendationRequest clovaRecommendationRequest) { + public static ClovaRequest createClovaRequest(AIRecommendationRequest aiRecommendationRequest) { ArrayList messages = new ArrayList<>(); - messages.add(Message.createSystem()); - messages.add(Message.createMessage(clovaRecommendationRequest)); + messages.add(Message.createSystem(ClovaStudioPrompt.DEFAULT_SYSTEM_PROMPT)); + messages.add(Message.createUserMessage(aiRecommendationRequest)); return ClovaRequest.builder() .messages(messages) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/dto/Role.java b/src/main/java/spring/backend/recommendation/infrastructure/dto/Role.java new file mode 100644 index 000000000..b53834a8d --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/dto/Role.java @@ -0,0 +1,11 @@ +package spring.backend.recommendation.infrastructure.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum Role { + SYSTEM("system"), USER("user"); + private final String description; +} diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java index 657b96bdc..c75bb2972 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java @@ -12,7 +12,7 @@ import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; import spring.backend.recommendation.application.GetRecommendationsFromClovaService; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.presentation.swagger.GetRecommendationsFromClovaSwagger; @@ -27,8 +27,8 @@ public class GetRecommendationsFromClovaController implements GetRecommendations @Authorization @PostMapping - public ResponseEntity>> requestRecommendations(@AuthorizedMember Member member, @Valid @RequestBody ClovaRecommendationRequest clovaRecommendationRequest) { - List response = getRecommendationsFromClovaService.getRecommendationsFromClova(clovaRecommendationRequest); + public ResponseEntity>> requestRecommendations(@AuthorizedMember Member member, @Valid @RequestBody AIRecommendationRequest aiRecommendationRequest) { + List response = getRecommendationsFromClovaService.getRecommendationsFromClova(aiRecommendationRequest); return ResponseEntity.ok(new RestResponse<>(response)); } } diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java index c75a79f2a..a49a2c30c 100644 --- a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java @@ -1,6 +1,5 @@ package spring.backend.recommendation.presentation.swagger; -import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -9,7 +8,7 @@ import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @@ -24,5 +23,5 @@ public interface GetRecommendationsFromClovaSwagger { operationId = "/v1/recommendations" ) @ApiErrorCode({GlobalErrorCode.class, ClovaErrorCode.class}) - ResponseEntity>> requestRecommendations(@Parameter(hidden = true) Member member, ClovaRecommendationRequest clovaRecommendationRequest); + ResponseEntity>> requestRecommendations(@Parameter(hidden = true) Member member, AIRecommendationRequest aiRecommendationRequest); } From 3beb2c05082a16f19dcd8bf42683a0bcc4bf2b97 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 01:21:16 +0900 Subject: [PATCH 260/478] =?UTF-8?q?fix:=20(#91)=20Message=EC=9D=98=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/Message.java | 70 ------------------ .../infrastructure/dto/Message.java | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 70 deletions(-) delete mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java deleted file mode 100644 index 8b9053552..000000000 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/Message.java +++ /dev/null @@ -1,70 +0,0 @@ -package spring.backend.recommendation.infrastructure.clova.dto.request; - - -import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.domain.value.Type; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; -import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; - -import java.util.Arrays; -import java.util.stream.Collectors; - -@Getter -@Builder -public class Message { - private String role; - private String content; - - @RequiredArgsConstructor - @Getter - public enum Role { - SYSTEM("system"), USER("user"); - private final String description; - } - - public static Message createSystem() { - return Message.builder() - .role(Role.SYSTEM.getDescription()) - .content(ClovaStudioPrompt.DEFAULT_SYSTEM_PROMPT) - .build(); - } - - public static Message createMessage(ClovaRecommendationRequest clovaRecommendationRequest) { - return Message.builder().role(Role.USER.getDescription()).content(createContent(clovaRecommendationRequest)).build(); - } - - private static String createContent(ClovaRecommendationRequest clovaRecommendationRequest) { - int spareTime = clovaRecommendationRequest.spareTime(); - Type activityType = clovaRecommendationRequest.activityType(); - String keywords = parseKeywords(clovaRecommendationRequest.keywords()); - String location = clovaRecommendationRequest.location(); - - if (isActivityTypeOfflineOrOnlineAndOffline(activityType, location)) { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); - } else { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); - } - } - - private static boolean isActivityTypeOfflineOrOnlineAndOffline(Type activityType, String location) { - if (activityType.equals(Type.OFFLINE) && location == null || activityType.equals(Type.ONLINE_AND_OFFLINE) && location == null) { - throw ClovaErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); - } - return activityType.equals(Type.OFFLINE) || activityType.equals(Type.ONLINE_AND_OFFLINE); - } - - private static String parseKeywords(Keyword.Category[] keywords) { - if (keywords.length == 0) { - return Arrays.stream(Keyword.Category.values()) - .map(Keyword.Category::getDescription) - .collect(Collectors.joining(", ")); - } else { - return Arrays.stream(keywords) - .map(Keyword.Category::getDescription) - .collect(Collectors.joining(", ")); - } - } -} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java new file mode 100644 index 000000000..e9ed7cd5e --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java @@ -0,0 +1,73 @@ +package spring.backend.recommendation.infrastructure.dto; + + +import lombok.Builder; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Type; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static spring.backend.activity.domain.value.Type.OFFLINE; +import static spring.backend.activity.domain.value.Type.ONLINE_AND_OFFLINE; +import static spring.backend.recommendation.infrastructure.dto.Role.SYSTEM; +import static spring.backend.recommendation.infrastructure.dto.Role.USER; + +@Builder +public record Message(String role, String content) { + public static Message createSystem(String prompt) { + return Message.builder() + .role(SYSTEM.getDescription()) + .content(prompt) + .build(); + } + + public static Message createUserMessage(AIRecommendationRequest aiRecommendationRequest) { + return Message.builder() + .role(USER.getDescription()) + .content(createContent(aiRecommendationRequest)) + .build(); + } + + private static String createContent(AIRecommendationRequest aiRecommendationRequest) { + int spareTime = aiRecommendationRequest.spareTime(); + Type activityType = aiRecommendationRequest.activityType(); + String keywords = parseKeywords(aiRecommendationRequest.keywords()); + String location = aiRecommendationRequest.location(); + + if (isActivityTypeOfflineOrOnlineAndOffline(activityType, location)) { + return createContentForActivityTypeOfflineOrOnlineAndOffline(spareTime, activityType, keywords, location); + } else { + return createContentForActivityTypeOnline(spareTime, activityType, keywords); + } + } + + private static String createContentForActivityTypeOnline(int spareTime, Type activityType, String keywords) { + return String.format("\"자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); + } + + private static String createContentForActivityTypeOfflineOrOnlineAndOffline(int spareTime, Type activityType, String keywords, String location) { + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); + } + + private static boolean isActivityTypeOfflineOrOnlineAndOffline(Type activityType, String location) { + if (activityType.equals(OFFLINE) && location == null || activityType.equals(ONLINE_AND_OFFLINE) && location == null) { + throw ActivityErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); + } + return activityType.equals(OFFLINE) || activityType.equals(ONLINE_AND_OFFLINE); + } + + private static String parseKeywords(Keyword.Category[] keywords) { + if (keywords.length == 0) { + return Arrays.stream(Keyword.Category.values()) + .map(Keyword.Category::getDescription) + .collect(Collectors.joining(", ")); + } else { + return Arrays.stream(keywords) + .map(Keyword.Category::getDescription) + .collect(Collectors.joining(", ")); + } + } +} From c26448a81156fc53866980ff839487b4f8e889ed Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 01:23:11 +0900 Subject: [PATCH 261/478] =?UTF-8?q?fix:=20(#91)=20RecommendationProvider?= =?UTF-8?q?=20=EC=9D=98=20DIP=20=EC=A0=81=EC=9A=A9=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20Clova=EC=99=80=EC=9D=98=20=EA=B2=B0=ED=95=A9?= =?UTF-8?q?=EC=9D=84=20=EB=B6=84=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 58 ++++++++++++++----- .../ClovaRecommendationProvider.java | 41 ++++++++++--- ...etRecommendationsFromClovaServiceTest.java | 6 +- 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 0de36b7ff..f43b02b73 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -4,9 +4,10 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.domain.value.Type; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import java.util.ArrayList; @@ -15,6 +16,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static spring.backend.activity.domain.value.Type.*; + @Service @Log4j2 @RequiredArgsConstructor @@ -34,25 +37,26 @@ public class GetRecommendationsFromClovaService { private static final String RELAXATION = "휴식"; private static final String SOCIAL = "소셜"; - private final RecommendationProvider recommendationProvider; + private final RecommendationProvider recommendationProvider; - public List getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { - List clovaResponses = fetchRecommendations(clovaRecommendationRequest); + public List getRecommendationsFromClova(AIRecommendationRequest aiRecommendationRequest) { + validateLocation(aiRecommendationRequest); + List clovaResponses = fetchRecommendations(aiRecommendationRequest); int attempt = 1; while (containsInvalidKeyword(clovaResponses) && attempt <= MAX_ATTEMPTS) { log.warn("추천활동의 키워드가 올바르지 않습니다. 재시도 횟수: {}/{}", attempt, MAX_ATTEMPTS); - clovaResponses = fetchRecommendations(clovaRecommendationRequest); + clovaResponses = fetchRecommendations(aiRecommendationRequest); attempt++; } - List validRecommedations = filteredValidRecommendations(clovaResponses); + List validRecommendations = filteredValidRecommendations(clovaResponses); - if (validRecommedations.isEmpty()) { + if (validRecommendations.isEmpty()) { throw ClovaErrorCode.INVALID_KEYWORD_IN_RECOMMENDATIONS.toException(); } - return validRecommedations; + return validRecommendations; } List filteredValidRecommendations(List clovaResponses) { @@ -60,10 +64,13 @@ List filteredValidRecommendations(List clovaResponse.getKeywordCategory() != null && isValidKeywordCategory(clovaResponse.getKeywordCategory())).collect(Collectors.toList()); } + public List fetchRecommendations(AIRecommendationRequest aiRecommendationRequest) { + validateClovaRecommendationRequestKeyword(aiRecommendationRequest); + ClovaResponse clovaResponse = recommendationProvider.getRecommendations(aiRecommendationRequest); + validateClovaResponse(clovaResponse); + String parsedClovaResponse = clovaResponse.getResult().getMessage().getContent(); - public List fetchRecommendations(ClovaRecommendationRequest clovaRecommendationRequest) { - validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); - String[] recommendations = recommendationProvider.requestToClovaStudio(clovaRecommendationRequest).split(LINE_SEPARATOR); + String[] recommendations = parsedClovaResponse.split(LINE_SEPARATOR); List clovaResponses = new ArrayList<>(); int order = 1; @@ -98,6 +105,27 @@ public List fetchRecommendations(ClovaRecommendatio return clovaResponses; } + private void validateLocation(AIRecommendationRequest aiRecommendationRequest) { + if ((aiRecommendationRequest.activityType() == OFFLINE || aiRecommendationRequest.activityType() == ONLINE_AND_OFFLINE) && + (aiRecommendationRequest.location() == null || aiRecommendationRequest.location().isEmpty())) { + log.error("[AIRecommendationRequest] location must exist when activityType is OFFLINE or ONLINE_AND_OFFLINE"); + throw ActivityErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); + } + + if (aiRecommendationRequest.activityType() == ONLINE && aiRecommendationRequest.location() != null && !aiRecommendationRequest.location().isEmpty()) { + log.error("[AIRecommendationRequest] location must not exist when activityType is ONLINE"); + throw ActivityErrorCode.EXIST_LOCATION_WHEN_ONLINE.toException(); + } + } + + private void validateClovaResponse(ClovaResponse clovaResponse) { + if (clovaResponse == null || clovaResponse.getResult() == null || clovaResponse.getResult().getMessage() == null || clovaResponse.getResult().getMessage().getContent() == null) { + log.error("Clova 서비스로부터 null 응답을 수신했습니다."); + throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); + } + + } + private boolean containsInvalidKeyword(List clovaResponses) { return clovaResponses.stream().anyMatch(clovaResponse -> clovaResponse.getKeywordCategory() == null @@ -108,12 +136,12 @@ private boolean isValidKeywordCategory(Keyword.Category keywordCategory) { return Arrays.stream(Keyword.Category.values()).anyMatch(category -> category == keywordCategory); } - private void validateClovaRecommendationRequestKeyword(ClovaRecommendationRequest clovaRecommendationRequest) { - if (clovaRecommendationRequest.activityType().equals(Type.ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.NATURE) + private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest clovaRecommendationRequest) { + if (clovaRecommendationRequest.activityType().equals(ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.NATURE) ) { throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); } - if (clovaRecommendationRequest.activityType().equals(Type.OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.SOCIAL) + if (clovaRecommendationRequest.activityType().equals(OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.SOCIAL) ) { throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index abc8545fd..75fdd261e 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -2,28 +2,51 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; +import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.application.RecommendationProvider; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Component @RequiredArgsConstructor @Log4j2 -public class ClovaRecommendationProvider implements RecommendationProvider { +public class ClovaRecommendationProvider implements RecommendationProvider { - private final ClovaService clovaService; + @Value("${clova.api.url}") + private String apiUrl; + + private final WebClient clovaStudioWebClient; @Override - public String requestToClovaStudio(ClovaRecommendationRequest clovaRecommendationRequest) { - ClovaResponse result = clovaService.requestToClovaStudio(clovaRecommendationRequest); + public ClovaResponse getRecommendations(AIRecommendationRequest aiRecommendationRequest) { + try { + ClovaRequest request = ClovaRequest.createClovaRequest(aiRecommendationRequest); - if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { - log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); - throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); + return clovaStudioWebClient.post() + .uri(apiUrl) + .bodyValue(request) + .retrieve() + .bodyToMono(ClovaResponse.class) + .block(); + } catch (WebClientException e) { + log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e); + throw GlobalErrorCode.WEB_CLIENT_ERROR.toException(); + } catch (Exception e) { + log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); + throw GlobalErrorCode.INTERNAL_ERROR.toException(); } - return result.getResult().getMessage().getContent(); +// if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { +// log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); +// throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); +// } +// +// return result; } } diff --git a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java index 7b8cf3c5b..b79f79a82 100644 --- a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java +++ b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java @@ -7,7 +7,7 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.core.exception.DomainException; -import spring.backend.recommendation.dto.request.ClovaRecommendationRequest; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,7 +23,7 @@ public class GetRecommendationsFromClovaServiceTest { @DisplayName("타입이 ONLINE인데 Keywords에 NATURE가 있는 경우 예외를 반환한다.") void throwExceptionIfOnlineActivityContainsNatureKeyword() { // GIVEN - ClovaRecommendationRequest request = new ClovaRecommendationRequest( + AIRecommendationRequest request = new AIRecommendationRequest( 300, Type.ONLINE, new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, @@ -40,7 +40,7 @@ void throwExceptionIfOnlineActivityContainsNatureKeyword() { @DisplayName("타입이 OFFLINE인데 Keywords에 SOCIAL가 있는 경우 예외를 반환한다.") void throwExceptionIfOfflineActivityContainsSocialKeyword() { // GIVEN - ClovaRecommendationRequest request = new ClovaRecommendationRequest( + AIRecommendationRequest request = new AIRecommendationRequest( 300, Type.OFFLINE, new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, From 7ec0c93c6f0f1f1a324a77e06983056f5f1b02fb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 12:59:52 +0900 Subject: [PATCH 262/478] =?UTF-8?q?refactor:=20(#91)=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8F=B4=EB=8D=94=EB=A5=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webclient/WebClientConfiguration.java | 28 ------------- .../clova/application/ClovaService.java | 42 ------------------- 2 files changed, 70 deletions(-) delete mode 100644 src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java delete mode 100644 src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java diff --git a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java b/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java deleted file mode 100644 index efeeecda7..000000000 --- a/src/main/java/spring/backend/core/configuration/webclient/WebClientConfiguration.java +++ /dev/null @@ -1,28 +0,0 @@ -package spring.backend.core.configuration.webclient; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfiguration { - - @Value("${clova.api.api-key}") - private String apiKey; - - @Value("${clova.api.api-gateway-key}") - private String apiGatewayKey; - - @Bean - public WebClient clovaStudioWebClient() { - return WebClient.builder() - .defaultHeaders(httpHeaders -> { - httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); - httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); - httpHeaders.setContentType(MediaType.APPLICATION_JSON); - }) - .build(); - } -} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java deleted file mode 100644 index ddda1795f..000000000 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaService.java +++ /dev/null @@ -1,42 +0,0 @@ -package spring.backend.recommendation.infrastructure.clova.application; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientException; -import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; -import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; - -@Service -@RequiredArgsConstructor -@Log4j2 -public class ClovaService { - - @Value("${clova.api.url}") - private String apiUrl; - - private final WebClient clovaStudioWebClient; - - public ClovaResponse requestToClovaStudio(AIRecommendationRequest aiRecommendationRequest) { - try { - ClovaRequest request = ClovaRequest.createClovaRequest(aiRecommendationRequest); - - return clovaStudioWebClient.post() - .uri(apiUrl) - .bodyValue(request) - .retrieve() - .bodyToMono(ClovaResponse.class) - .block(); - } catch (WebClientException e) { - log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e); - throw GlobalErrorCode.WEB_CLIENT_ERROR.toException(); - } catch (Exception e) { - log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); - throw GlobalErrorCode.INTERNAL_ERROR.toException(); - } - } -} From 309b0e5d24d2b15350ce61eb56dc684e7736798b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 13:00:21 +0900 Subject: [PATCH 263/478] =?UTF-8?q?refactor:=20(#91)=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B0=9C=ED=96=89=EA=B3=BC=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 1 - .../dto/request/AIRecommendationRequest.java | 30 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index f43b02b73..03f6acbb3 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -123,7 +123,6 @@ private void validateClovaResponse(ClovaResponse clovaResponse) { log.error("Clova 서비스로부터 null 응답을 수신했습니다."); throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); } - } private boolean containsInvalidKeyword(List clovaResponses) { diff --git a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java index 85d123816..c68de118d 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java @@ -4,25 +4,25 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import lombok.extern.log4j.Log4j2; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -@Log4j2 -public record AIRecommendationRequest(@NotNull(message = "자투리 시간은 필수 입력 항목입니다.") - @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") - @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") - @Schema(description = "자투리 시간", example = "30") - Integer spareTime, +public record AIRecommendationRequest( + @NotNull(message = "자투리 시간은 필수 입력 항목입니다.") + @Min(value = 10, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") + @Max(value = 300, message = "자투리 시간은 10부터 300 사이의 숫자로 입력해주세요.") + @Schema(description = "자투리 시간", example = "30") + Integer spareTime, - @NotNull(message = "활동 유형은 필수 입력 항목입니다.") - @Schema(description = "활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE 중 하나를 선택합니다.)", example = "OFFLINE") - Type activityType, + @NotNull(message = "활동 유형은 필수 입력 항목입니다.") + @Schema(description = "활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE 중 하나를 선택합니다.)", example = "OFFLINE") + Type activityType, - @NotNull(message = "키워드는 필수 입력 항목입니다.") - @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") - Keyword.Category[] keywords, + @NotNull(message = "키워드는 필수 입력 항목입니다.") + @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") + Keyword.Category[] keywords, - @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") - String location) { + @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") + String location +) { } From 9f2501a736495700d2978e717e47b2ae683721f5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 13:00:45 +0900 Subject: [PATCH 264/478] =?UTF-8?q?refactor:=20(#91)=20WebClient=EB=A5=BC?= =?UTF-8?q?=20Provider=EC=97=90=EC=84=9C=20=EC=A7=81=EC=A0=91=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClovaRecommendationProvider.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index 75fdd261e..e41390305 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; @@ -11,7 +12,6 @@ import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; -import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; @Component @RequiredArgsConstructor @@ -21,14 +21,27 @@ public class ClovaRecommendationProvider implements RecommendationProvider { + httpHeaders.set("X-NCP-CLOVASTUDIO-API-KEY", apiKey); + httpHeaders.set("X-NCP-APIGW-API-KEY", apiGatewayKey); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + }) + .build(); + + return webClient + .post() .uri(apiUrl) .bodyValue(request) .retrieve() @@ -41,12 +54,5 @@ public ClovaResponse getRecommendations(AIRecommendationRequest aiRecommendation log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); throw GlobalErrorCode.INTERNAL_ERROR.toException(); } - -// if (result == null || result.getResult() == null || result.getResult().getMessage() == null || result.getResult().getMessage().getContent() == null) { -// log.error("Clova 서비스로부터 null 응답을 수신했습니다. 요청 내용: {}", clovaRecommendationRequest); -// throw ClovaErrorCode.NULL_RESPONSE_FROM_CLOVA.toException(); -// } -// -// return result; } } From f32819f402c9a2f38067495d8c3ad6e4795fe79c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 14 Nov 2024 19:54:51 +0900 Subject: [PATCH 265/478] =?UTF-8?q?refactor:=20(#91)=20clova=EC=97=90=20?= =?UTF-8?q?=EC=A2=85=EC=86=8D=EB=90=9C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=A0=20Request=20dto=EC=9D=98=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20clovaRecommendationRequest=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 22 +++++++++---------- .../ClovaRecommendationProvider.java | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 03f6acbb3..6dc2e6aaf 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -39,14 +39,14 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; - public List getRecommendationsFromClova(AIRecommendationRequest aiRecommendationRequest) { - validateLocation(aiRecommendationRequest); - List clovaResponses = fetchRecommendations(aiRecommendationRequest); + public List getRecommendationsFromClova(AIRecommendationRequest clovaRecommendationRequest) { + validateLocation(clovaRecommendationRequest); + List clovaResponses = fetchRecommendations(clovaRecommendationRequest); int attempt = 1; while (containsInvalidKeyword(clovaResponses) && attempt <= MAX_ATTEMPTS) { log.warn("추천활동의 키워드가 올바르지 않습니다. 재시도 횟수: {}/{}", attempt, MAX_ATTEMPTS); - clovaResponses = fetchRecommendations(aiRecommendationRequest); + clovaResponses = fetchRecommendations(clovaRecommendationRequest); attempt++; } @@ -64,9 +64,9 @@ List filteredValidRecommendations(List clovaResponse.getKeywordCategory() != null && isValidKeywordCategory(clovaResponse.getKeywordCategory())).collect(Collectors.toList()); } - public List fetchRecommendations(AIRecommendationRequest aiRecommendationRequest) { - validateClovaRecommendationRequestKeyword(aiRecommendationRequest); - ClovaResponse clovaResponse = recommendationProvider.getRecommendations(aiRecommendationRequest); + public List fetchRecommendations(AIRecommendationRequest clovaRecommendationRequest) { + validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); + ClovaResponse clovaResponse = recommendationProvider.getRecommendations(clovaRecommendationRequest); validateClovaResponse(clovaResponse); String parsedClovaResponse = clovaResponse.getResult().getMessage().getContent(); @@ -105,14 +105,14 @@ public List fetchRecommendations(AIRecommendationRe return clovaResponses; } - private void validateLocation(AIRecommendationRequest aiRecommendationRequest) { - if ((aiRecommendationRequest.activityType() == OFFLINE || aiRecommendationRequest.activityType() == ONLINE_AND_OFFLINE) && - (aiRecommendationRequest.location() == null || aiRecommendationRequest.location().isEmpty())) { + private void validateLocation(AIRecommendationRequest clovaRecommendationRequest) { + if ((clovaRecommendationRequest.activityType() == OFFLINE || clovaRecommendationRequest.activityType() == ONLINE_AND_OFFLINE) && + (clovaRecommendationRequest.location() == null || clovaRecommendationRequest.location().isEmpty())) { log.error("[AIRecommendationRequest] location must exist when activityType is OFFLINE or ONLINE_AND_OFFLINE"); throw ActivityErrorCode.NOT_EXIST_LOCATION_WHEN_OFFLINE.toException(); } - if (aiRecommendationRequest.activityType() == ONLINE && aiRecommendationRequest.location() != null && !aiRecommendationRequest.location().isEmpty()) { + if (clovaRecommendationRequest.activityType() == ONLINE && clovaRecommendationRequest.location() != null && !clovaRecommendationRequest.location().isEmpty()) { log.error("[AIRecommendationRequest] location must not exist when activityType is ONLINE"); throw ActivityErrorCode.EXIST_LOCATION_WHEN_ONLINE.toException(); } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index e41390305..a5c3dd58d 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -28,9 +28,9 @@ public class ClovaRecommendationProvider implements RecommendationProvider { From ade74a619e95162b228583a9de22904f7ea12d15 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 10 Nov 2024 05:34:55 +0900 Subject: [PATCH 266/478] =?UTF-8?q?feat:=20(#81)=20OpenAI=20API=EB=A5=BC?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openai/OpenAIRecommendationProvider.java | 68 +++++++++++++++++++ .../openai/dto/request/OpenAIPrompt.java | 15 ++++ .../openai/dto/response/OpenAIResponse.java | 49 +++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/openai/dto/response/OpenAIResponse.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java new file mode 100644 index 000000000..89948c400 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java @@ -0,0 +1,68 @@ +package spring.backend.recommendation.infrastructure.openai; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; +import reactor.core.publisher.Mono; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.recommendation.application.RecommendationProvider; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.infrastructure.dto.Message; +import spring.backend.recommendation.infrastructure.openai.dto.request.OpenAIPrompt; +import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class OpenAIRecommendationProvider implements RecommendationProvider> { + + @Value("${openai.model}") + private String model; + + @Value("${openai.secret-key}") + private String secretKey; + + @Value("${openai.chat-completions-url}") + private String chatCompletionsUrl; + + public Mono getRecommendations(AIRecommendationRequest request) { + return WebClient.create() + .post() + .uri(chatCompletionsUrl) + .headers(header -> { + header.setContentType(MediaType.APPLICATION_JSON); + header.setBearerAuth(secretKey); + }) + .bodyValue(createRecommendationRequestBody(request)) + .retrieve() + .bodyToMono(OpenAIResponse.class) + .onErrorResume(WebClientException.class, e -> { + log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e); + return Mono.error(GlobalErrorCode.WEB_CLIENT_ERROR.toException()); + }) + .onErrorResume(Exception.class, e -> { + log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e); + return Mono.error(GlobalErrorCode.INTERNAL_ERROR.toException()); + }); + } + + private Map createRecommendationRequestBody(AIRecommendationRequest request) { + Map requestBody = new HashMap<>(); + requestBody.put("model", model); + + List messages = new ArrayList<>(); + messages.add(Message.createSystem(OpenAIPrompt.DEFAULT_SYSTEM_PROMPT)); + messages.add(Message.createUserMessage(request)); + requestBody.put("messages", messages); + return requestBody; + } +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java new file mode 100644 index 000000000..50976c3ed --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java @@ -0,0 +1,15 @@ +package spring.backend.recommendation.infrastructure.openai.dto.request; + +public class OpenAIPrompt { + + public static final String DEFAULT_SYSTEM_PROMPT = """ + 자투리 시간을 잘 보낼 수 있는 활동을 아래 상황에 맞춰서 구체적으로 추천해줘. + '자기개발'이란 '시사상식, 지식, 교양 측면의 활동들, 지식과 능력을 확장하고 개인의 성장과 발전을 위한 활동'를 말해. + 사용할 수 있는 플랫폼도 같이 알려주면 좋을 거 같아. + 예시 답변은 + 제목 : 최근 흥행작 누구보다 빠르게 찾아보기! + 내용 : 메가박스에서 영화 '대도시의 사랑법' 관람하기 + 플랫폼 : 메가박스 + 이거처럼 작성해주고, 제목은 예시 답변처럼 앞에 형용하는 멋진 문장으로 써줘 + """; +} diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/response/OpenAIResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/response/OpenAIResponse.java new file mode 100644 index 000000000..a85592455 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/response/OpenAIResponse.java @@ -0,0 +1,49 @@ +package spring.backend.recommendation.infrastructure.openai.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import spring.backend.recommendation.infrastructure.dto.Message; + +import java.util.List; + +public record OpenAIResponse( + String id, + String object, + int created, + String model, + @JsonProperty("system_fingerprint") + String systemFingerprint, + List choices, + Usage usage +) { + + public record Choice( + int index, + Message message, + Boolean logprobs, + @JsonProperty("finish_reason") + String finishReason + ) { + } + + public record Usage( + @JsonProperty("prompt_tokens") + int promptTokens, + @JsonProperty("completion_tokens") + int completionTokens, + @JsonProperty("total_tokens") + int totalTokens, + @JsonProperty("completion_tokens_details") + CompletionTokensDetails completionTokensDetails + ) { + } + + public record CompletionTokensDetails( + @JsonProperty("reasoning_tokens") + int reasoningTokens, + @JsonProperty("accepted_prediction_tokens") + int acceptedPredictionTokens, + @JsonProperty("rejected_prediction_tokens") + int rejectedPredictionTokens + ) { + } +} From eeac514fe1731bfa6e9ed1511f1ef9ab4ca82024 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 17 Nov 2024 04:33:41 +0900 Subject: [PATCH 267/478] =?UTF-8?q?feat:=20(#90)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=A1=B0=ED=9A=8C=20Query?= =?UTF-8?q?=EB=A5=BC=20DAO=EC=97=90=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/ActivityCalendarResponse.java | 14 +++++++ .../response/UserMonthlyActivityDetail.java | 17 +++++++++ .../response/UserMonthlyActivitySummary.java | 13 +++++++ .../persistence/jpa/dao/ActivityJpaDao.java | 38 ++++++++++++++++++- .../activity/query/dao/ActivityDao.java | 6 +++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java create mode 100644 src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java create mode 100644 src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java new file mode 100644 index 000000000..9d6c74ee8 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java @@ -0,0 +1,14 @@ +package spring.backend.activity.dto.response; + +import java.util.List; + +public record ActivityCalendarResponse( + + UserMonthlyActivitySummary summary, + + List monthlyActivities +) { + public static ActivityCalendarResponse of(UserMonthlyActivitySummary summary, List details) { + return new ActivityCalendarResponse(summary, details); + } +} diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java new file mode 100644 index 000000000..1e6079552 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java @@ -0,0 +1,17 @@ +package spring.backend.activity.dto.response; + +import spring.backend.activity.domain.value.Keyword.Category; + +import java.time.LocalDateTime; + +public record UserMonthlyActivityDetail( + + Category category, + + String title, + + int savedTime, + + LocalDateTime activityCreatedAt +) { +} diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java new file mode 100644 index 000000000..22d00af82 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java @@ -0,0 +1,13 @@ +package spring.backend.activity.dto.response; + +import java.time.LocalDateTime; + +public record UserMonthlyActivitySummary( + + LocalDateTime registrationDate, + + long totalSavedTime, + + long monthlyActivityCount +) { +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index 365d2a467..ed28ad229 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -3,6 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.activity.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.dto.response.UserMonthlyActivitySummary; import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; import spring.backend.activity.query.dao.ActivityDao; @@ -27,4 +29,38 @@ public interface ActivityJpaDao extends JpaRepository, order by a.createdAt ASC """) List findTodayActivities(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); -} \ No newline at end of file + + @Override + @Query(""" + select new spring.backend.activity.dto.response.UserMonthlyActivitySummary( + m.createdAt, + coalesce(sum(a.savedTime), 0), + count(a) + ) + from MemberJpaEntity m + left join ActivityJpaEntity a on a.memberId = m.id + and a.finished = true + and function('year', a.createdAt) = :year + and function('month', a.createdAt) = :month + where m.id = :memberId + """) + UserMonthlyActivitySummary findActivitySummaryByYearAndMonth(UUID memberId, int year, int month); + + + @Override + @Query(""" + select new spring.backend.activity.dto.response.UserMonthlyActivityDetail( + a.keyword.category, + a.title, + a.savedTime, + a.createdAt + ) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.finished = true + and function('year', a.createdAt) = :year + and function('month', a.createdAt) = :month + order by a.createdAt desc + """) + List findActivityDetailsByYearAndMonth(UUID memberId, int year, int month); +} diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index be1e2bb39..90c033d93 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -1,6 +1,8 @@ package spring.backend.activity.query.dao; import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.activity.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.dto.response.UserMonthlyActivitySummary; import java.time.LocalDateTime; import java.util.List; @@ -9,4 +11,8 @@ public interface ActivityDao { List findTodayActivities(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + + UserMonthlyActivitySummary findActivitySummaryByYearAndMonth(UUID memberId, int year, int month); + + List findActivityDetailsByYearAndMonth(UUID memberId, int year, int month); } From 4cff85698ece6731d141952942983f977a0693fc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 17 Nov 2024 18:25:30 +0900 Subject: [PATCH 268/478] =?UTF-8?q?feat:=20(#90)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=A1=B0=ED=9A=8C=20API=EB=A5=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/ReadActivityCalendarRequest.java | 18 ++++++++++ .../ReadActivityCalendarController.java | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java create mode 100644 src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java diff --git a/src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java b/src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java new file mode 100644 index 000000000..db6289cd9 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java @@ -0,0 +1,18 @@ +package spring.backend.activity.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +public record ReadActivityCalendarRequest( + + @Min(value = 2024, message = "년도는 2024년 이상이어야 합니다.") + @Schema(description = "캘린더 조회 년도", example = "2024") + int year, + + @Min(value = 1, message = "월은 1~12 사이여야 합니다.") + @Max(value = 12, message = "월은 1~12 사이여야 합니다.") + @Schema(description = "캘린더 조회 월", example = "11") + int month +) { +} diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java b/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java new file mode 100644 index 000000000..830ba4d3a --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java @@ -0,0 +1,34 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.dto.request.ReadActivityCalendarRequest; +import spring.backend.activity.dto.response.ActivityCalendarResponse; +import spring.backend.activity.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.dto.response.UserMonthlyActivitySummary; +import spring.backend.activity.presentation.swagger.ReadActivityCalendarSwagger; +import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ReadActivityCalendarController implements ReadActivityCalendarSwagger { + + private final ActivityDao activityDao; + + @Authorization + @GetMapping("/v1/activity-calendar") + public ResponseEntity> readActivityCalendar(@AuthorizedMember Member member, @Valid ReadActivityCalendarRequest request) { + UserMonthlyActivitySummary summary = activityDao.findActivitySummaryByYearAndMonth(member.getId(), request.year(), request.month()); + List details = activityDao.findActivityDetailsByYearAndMonth(member.getId(), request.year(), request.month()); + return ResponseEntity.ok(new RestResponse<>(ActivityCalendarResponse.of(summary, details))); + } +} From 041a3b085c46840e86fae73f72551a2573351720 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 17 Nov 2024 18:26:31 +0900 Subject: [PATCH 269/478] =?UTF-8?q?feat:=20(#90)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=BA=98=EB=A6=B0=EB=8D=94=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/ActivityCalendarResponse.java | 6 +++++ .../response/UserMonthlyActivityDetail.java | 6 +++++ .../response/UserMonthlyActivitySummary.java | 5 ++++ .../swagger/ReadActivityCalendarSwagger.java | 26 +++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java index 9d6c74ee8..036325fe8 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java @@ -1,11 +1,17 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; public record ActivityCalendarResponse( + @Schema(description = "사용자의 월별 활동 요약", + example = "{ \"registrationDate\": \"2024-11-14T06:10:55.091954\", \"totalSavedTime\": 120, \"monthlyActivityCount\": 10 }") UserMonthlyActivitySummary summary, + @Schema(description = "사용자의 월별 활동 상세 정보 리스트", + example = "{ \"category\": \"SELF_DEVELOPMENT\", \"title\": \"마음의 편안을 가져다주는 명상음악 20분 듣기\", \"savedTime\": 20, \"activityCreatedAt\": \"2024-11-16T14:24:08.548712\" }") List monthlyActivities ) { public static ActivityCalendarResponse of(UserMonthlyActivitySummary summary, List details) { diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java index 1e6079552..c9cea3d3e 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java +++ b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java @@ -1,17 +1,23 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword.Category; import java.time.LocalDateTime; public record UserMonthlyActivityDetail( + @Schema(description = "활동 카테고리 \n\n SELF_DEVELOPMENT(자기개발), HEALTH(건강), NATURE(자연), CULTURE_ART(문화/예술), ENTERTAINMENT(엔터테인먼트), RELAXATION(휴식), SOCIAL(소셜)", + example = "SELF_DEVELOPMENT") Category category, + @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") String title, + @Schema(description = "모은 시간", example = "20") int savedTime, + @Schema(description = "활동 생성 시간", example = "2024-11-16T14:24:08.548712") LocalDateTime activityCreatedAt ) { } diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java index 22d00af82..fa7a0810f 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java +++ b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java @@ -1,13 +1,18 @@ package spring.backend.activity.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; + import java.time.LocalDateTime; public record UserMonthlyActivitySummary( + @Schema(description = "사용자 최초 가입 시간", example = "2024-11-14T06:10:55.091954") LocalDateTime registrationDate, + @Schema(description = "해당 달에 모은 시간 조각의 합 (분 단위)", example = "120") long totalSavedTime, + @Schema(description = "사용자가 해당 달에 한 활동의 총 횟수", example = "10") long monthlyActivityCount ) { } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java new file mode 100644 index 000000000..03166ac95 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java @@ -0,0 +1,26 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.ReadActivityCalendarRequest; +import spring.backend.activity.dto.response.ActivityCalendarResponse; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "Activity", description = "활동") +public interface ReadActivityCalendarSwagger { + + @Operation( + summary = "활동 캘린더 조회 API", + description = "사용자가 연월을 선택하여 월별 활동 요약과 월별 활동 상세 정보 리스트를 반환합니다.", + operationId = "/v1/activity-calendar" + ) + @ApiErrorCode({GlobalErrorCode.class, ActivityErrorCode.class}) + ResponseEntity> readActivityCalendar(@Parameter(hidden = true) Member member, @ParameterObject ReadActivityCalendarRequest request); +} From 54550ba8b4ca6db215d7eaced0502c1677b15219 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 05:28:25 +0900 Subject: [PATCH 270/478] =?UTF-8?q?refactor:=20(#78)=20property=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20oauth=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EC=97=AC=20oau?= =?UTF-8?q?th=20property=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/google/GoogleOAuthRestClient.java | 2 +- .../auth/infrastructure/kakao/KakaoOAuthRestClient.java | 2 +- .../auth/infrastructure/naver/NaverOAuthRestClient.java | 2 +- .../property/{ => oauth}/GoogleOAuthProperty.java | 4 ++-- .../property/{ => oauth}/KakaoOAuthProperty.java | 4 ++-- .../property/{ => oauth}/NaverOAuthProperty.java | 4 ++-- .../property/{ => oauth}/shared/BaseOAuthProperty.java | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename src/main/java/spring/backend/core/configuration/property/{ => oauth}/GoogleOAuthProperty.java (68%) rename src/main/java/spring/backend/core/configuration/property/{ => oauth}/KakaoOAuthProperty.java (68%) rename src/main/java/spring/backend/core/configuration/property/{ => oauth}/NaverOAuthProperty.java (68%) rename src/main/java/spring/backend/core/configuration/property/{ => oauth}/shared/BaseOAuthProperty.java (84%) diff --git a/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java index 76b09c1ba..78392823b 100644 --- a/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java @@ -12,7 +12,7 @@ import spring.backend.auth.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; -import spring.backend.core.configuration.property.GoogleOAuthProperty; +import spring.backend.core.configuration.property.oauth.GoogleOAuthProperty; import java.net.URI; diff --git a/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java index c69fe3330..ff91dd14b 100644 --- a/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java @@ -13,7 +13,7 @@ import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.kakao.dto.KakaoResourceResponse; -import spring.backend.core.configuration.property.KakaoOAuthProperty; +import spring.backend.core.configuration.property.oauth.KakaoOAuthProperty; import java.net.URI; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java index 9ad948fde..1aa95f285 100644 --- a/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java @@ -13,7 +13,7 @@ import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.naver.dto.NaverResourceResponse; -import spring.backend.core.configuration.property.NaverOAuthProperty; +import spring.backend.core.configuration.property.oauth.NaverOAuthProperty; import java.math.BigInteger; import java.net.URI; diff --git a/src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/oauth/GoogleOAuthProperty.java similarity index 68% rename from src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java rename to src/main/java/spring/backend/core/configuration/property/oauth/GoogleOAuthProperty.java index 58f3932e0..29a35f815 100644 --- a/src/main/java/spring/backend/core/configuration/property/GoogleOAuthProperty.java +++ b/src/main/java/spring/backend/core/configuration/property/oauth/GoogleOAuthProperty.java @@ -1,10 +1,10 @@ -package spring.backend.core.configuration.property; +package spring.backend.core.configuration.property.oauth; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import spring.backend.core.configuration.property.shared.BaseOAuthProperty; +import spring.backend.core.configuration.property.oauth.shared.BaseOAuthProperty; @Component @Getter diff --git a/src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/oauth/KakaoOAuthProperty.java similarity index 68% rename from src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java rename to src/main/java/spring/backend/core/configuration/property/oauth/KakaoOAuthProperty.java index 5d61d853b..e316cd3fa 100644 --- a/src/main/java/spring/backend/core/configuration/property/KakaoOAuthProperty.java +++ b/src/main/java/spring/backend/core/configuration/property/oauth/KakaoOAuthProperty.java @@ -1,10 +1,10 @@ -package spring.backend.core.configuration.property; +package spring.backend.core.configuration.property.oauth; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import spring.backend.core.configuration.property.shared.BaseOAuthProperty; +import spring.backend.core.configuration.property.oauth.shared.BaseOAuthProperty; @Component @Getter diff --git a/src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/oauth/NaverOAuthProperty.java similarity index 68% rename from src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java rename to src/main/java/spring/backend/core/configuration/property/oauth/NaverOAuthProperty.java index 8fcc32be1..c7445eb41 100644 --- a/src/main/java/spring/backend/core/configuration/property/NaverOAuthProperty.java +++ b/src/main/java/spring/backend/core/configuration/property/oauth/NaverOAuthProperty.java @@ -1,10 +1,10 @@ -package spring.backend.core.configuration.property; +package spring.backend.core.configuration.property.oauth; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import spring.backend.core.configuration.property.shared.BaseOAuthProperty; +import spring.backend.core.configuration.property.oauth.shared.BaseOAuthProperty; @Component @Getter diff --git a/src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java b/src/main/java/spring/backend/core/configuration/property/oauth/shared/BaseOAuthProperty.java similarity index 84% rename from src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java rename to src/main/java/spring/backend/core/configuration/property/oauth/shared/BaseOAuthProperty.java index f272f2572..ce10e3901 100644 --- a/src/main/java/spring/backend/core/configuration/property/shared/BaseOAuthProperty.java +++ b/src/main/java/spring/backend/core/configuration/property/oauth/shared/BaseOAuthProperty.java @@ -1,4 +1,4 @@ -package spring.backend.core.configuration.property.shared; +package spring.backend.core.configuration.property.oauth.shared; import lombok.Getter; import lombok.Setter; From ee6fb06f97a2abf8d6c027c81f66c261ff50f349 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:47:36 +0900 Subject: [PATCH 271/478] =?UTF-8?q?feat:=20(#78)=20RabbitMQ=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 90fa430c5..ae8b05a1c 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,9 @@ dependencies { // Email implementation 'org.springframework.boot:spring-boot-starter-mail' + // Rabbit MQ + implementation 'org.springframework.boot:spring-boot-starter-amqp' + testImplementation 'org.springframework.amqp:spring-rabbit-test' } tasks.named('test') { From 67c933de81886f9b83b4959fd45d4e15d78d2f0c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:48:25 +0900 Subject: [PATCH 272/478] =?UTF-8?q?feat:=20(#78)=20RabbitMQ=20Delayed=20Me?= =?UTF-8?q?ssage=20Plugin=EC=9D=84=20Docker=20Compose=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-local.yml | 16 ++++++++++++++++ docker-compose.yml | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 7870552c3..7321597fd 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -21,8 +21,24 @@ services: networks: - cnergy-backend-network + rabbit: + container_name: cnergy-rabbitmq + hostname: cnergy-rabbit + image: heidiks/rabbitmq-delayed-message-exchange:4.0.2-management + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=password + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq-data:/var/lib/rabbitmq + networks: + - cnergy-backend-network + volumes: redis-data: + rabbitmq-data: networks: cnergy-backend-network: diff --git a/docker-compose.yml b/docker-compose.yml index 3aa3c2f6e..553d17770 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,26 @@ services: networks: - cnergy-backend-network + rabbit: + container_name: cnergy-rabbitmq + hostname: cnergy-rabbit + image: heidiks/rabbitmq-delayed-message-exchange:4.0.2-management + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + ports: + - "5672:5672" + - "15672:15672" + env_file: + - .env + volumes: + - rabbitmq-data:/var/lib/rabbitmq + networks: + - cnergy-backend-network + volumes: redis-data: + rabbitmq-data: networks: cnergy-backend-network: From 805350d8ae67ea1821c6b2a542c78c4f80de3771 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:49:28 +0900 Subject: [PATCH 273/478] =?UTF-8?q?feat:=20(#78)=20ApplicationContext?= =?UTF-8?q?=EC=97=90=20=EB=93=B1=EB=A1=9D=EB=90=9C=20=EB=B9=88=EC=9D=84=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=AC=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicationContextProvider.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/ApplicationContextProvider.java diff --git a/src/main/java/spring/backend/core/configuration/ApplicationContextProvider.java b/src/main/java/spring/backend/core/configuration/ApplicationContextProvider.java new file mode 100644 index 000000000..d7de49c2a --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/ApplicationContextProvider.java @@ -0,0 +1,21 @@ +package spring.backend.core.configuration; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationContextProvider implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + public static T getBean(String name, Class requiredType) { + return applicationContext.getBean(name, requiredType); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextProvider.applicationContext = applicationContext; + } +} From c632a08fd35f460caa476279801c22c0b05e2398 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:50:18 +0900 Subject: [PATCH 274/478] =?UTF-8?q?feat:=20(#78)=20RabbitMQ=20Configuratio?= =?UTF-8?q?n=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/RabbitMQConfiguration.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/RabbitMQConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/RabbitMQConfiguration.java b/src/main/java/spring/backend/core/configuration/RabbitMQConfiguration.java new file mode 100644 index 000000000..9c6c32c2f --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/RabbitMQConfiguration.java @@ -0,0 +1,71 @@ +package spring.backend.core.configuration; + +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.CustomExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import spring.backend.core.configuration.property.queue.FinishActivityQueueProperty; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@RequiredArgsConstructor +public class RabbitMQConfiguration { + + private final RabbitProperties rabbitProperties; + + private final FinishActivityQueueProperty finishActivityQueueProperty; + + @Bean + RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); + return rabbitTemplate; + } + + @Bean + public MessageConverter rabbitMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + public ConnectionFactory rabbitConnectionFactory() { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + connectionFactory.setHost(rabbitProperties.getHost()); + connectionFactory.setPort(rabbitProperties.getPort()); + connectionFactory.setUsername(rabbitProperties.getUsername()); + connectionFactory.setPassword(rabbitProperties.getPassword()); + connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL); + return connectionFactory; + } + + @Bean + public CustomExchange finishActivityExchange() { + Map args = new HashMap<>(); + args.put("x-delayed-type", "direct"); + return new CustomExchange(finishActivityQueueProperty.getExchange(), "x-delayed-message", true, false, args); + } + + @Bean + Queue finishActivityQueue() { + return new Queue(finishActivityQueueProperty.getQueue(), false); + } + + @Bean + Binding bindingFinishActivityQueue(CustomExchange finishActivityExchange) { + return BindingBuilder.bind(finishActivityQueue()) + .to(finishActivityExchange) + .with(finishActivityQueueProperty.getRoutingKey()) + .noargs(); + } +} From 45e7f03a7ce9c5e6203e3284f06eccdf2dc17693 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:52:33 +0900 Subject: [PATCH 275/478] =?UTF-8?q?feat:=20(#78)=20RabbitMQ=EC=9D=98=20Pro?= =?UTF-8?q?perty,=20Producer,=20Consumer=20=EA=B8=B0=EC=B4=88=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../queue/shared/BaseQueueProperty.java | 15 +++++++ .../infrastructure/queue/MessageConsumer.java | 6 +++ .../infrastructure/queue/MessageProducer.java | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/property/queue/shared/BaseQueueProperty.java create mode 100644 src/main/java/spring/backend/core/infrastructure/queue/MessageConsumer.java create mode 100644 src/main/java/spring/backend/core/infrastructure/queue/MessageProducer.java diff --git a/src/main/java/spring/backend/core/configuration/property/queue/shared/BaseQueueProperty.java b/src/main/java/spring/backend/core/configuration/property/queue/shared/BaseQueueProperty.java new file mode 100644 index 000000000..352ecede3 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/queue/shared/BaseQueueProperty.java @@ -0,0 +1,15 @@ +package spring.backend.core.configuration.property.queue.shared; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BaseQueueProperty { + + protected String exchange; + + protected String queue; + + protected String routingKey; +} diff --git a/src/main/java/spring/backend/core/infrastructure/queue/MessageConsumer.java b/src/main/java/spring/backend/core/infrastructure/queue/MessageConsumer.java new file mode 100644 index 000000000..eb7ed1913 --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/queue/MessageConsumer.java @@ -0,0 +1,6 @@ +package spring.backend.core.infrastructure.queue; + +public interface MessageConsumer { + + void consumeMessage(T message); +} diff --git a/src/main/java/spring/backend/core/infrastructure/queue/MessageProducer.java b/src/main/java/spring/backend/core/infrastructure/queue/MessageProducer.java new file mode 100644 index 000000000..6f6f637b2 --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/queue/MessageProducer.java @@ -0,0 +1,42 @@ +package spring.backend.core.infrastructure.queue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import spring.backend.core.configuration.ApplicationContextProvider; +import spring.backend.core.configuration.property.queue.shared.BaseQueueProperty; + +@RequiredArgsConstructor +@Log4j2 +public abstract class MessageProducer { + + private static final RabbitTemplate rabbitTemplate = ApplicationContextProvider.getBean("rabbitTemplate", RabbitTemplate.class); + + protected final T queueProperty; + + protected final R message; + + public void publishMessage() { + try { + rabbitTemplate.convertAndSend(queueProperty.getExchange(), queueProperty.getRoutingKey(), message); + } catch (Exception e) { + log.error("[MessagePublisher] - publishMessage Failed", e); + } + } + + public void publishMessageWithDelay(long delayTime) { + try { + rabbitTemplate.convertAndSend( + queueProperty.getExchange(), + queueProperty.getRoutingKey(), + message, + messagePostProcessor -> { + messagePostProcessor.getMessageProperties().setDelayLong(delayTime); + return messagePostProcessor; + } + ); + } catch (Exception e) { + log.error("[MessagePublisher] - publishMessageWithDelay Failed", e); + } + } +} From 971702ef3247644b1a7a5253782f3cf415e9fbde Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:57:38 +0900 Subject: [PATCH 276/478] =?UTF-8?q?feat:=20(#78)=20=ED=99=9C=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EC=9E=90=ED=88=AC=EB=A6=AC=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A2=85=EB=A3=8C=20=ED=81=90=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EB=B0=9C=ED=96=89=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinishActivityAutoService.java | 28 +++++++++++++++++++ .../QuickStartActivitySelectService.java | 5 ++-- .../UserActivitySelectService.java | 4 ++- .../queue/FinishActivityMessage.java | 10 +++++++ .../queue/FinishActivityMessageProducer.java | 11 ++++++++ .../queue/FinishActivityQueueProperty.java | 10 +++++++ 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/main/java/spring/backend/activity/application/FinishActivityAutoService.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessage.java create mode 100644 src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageProducer.java create mode 100644 src/main/java/spring/backend/core/configuration/property/queue/FinishActivityQueueProperty.java diff --git a/src/main/java/spring/backend/activity/application/FinishActivityAutoService.java b/src/main/java/spring/backend/activity/application/FinishActivityAutoService.java new file mode 100644 index 000000000..b925dfada --- /dev/null +++ b/src/main/java/spring/backend/activity/application/FinishActivityAutoService.java @@ -0,0 +1,28 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.infrastructure.queue.FinishActivityMessage; +import spring.backend.activity.infrastructure.queue.FinishActivityMessageProducer; +import spring.backend.core.configuration.property.queue.FinishActivityQueueProperty; + +@Service +@RequiredArgsConstructor +public class FinishActivityAutoService { + + private final FinishActivityQueueProperty finishActivityQueueProperty; + + public void finishActivityAuto(Activity activity) { + int spareTime = activity.getSpareTime(); + FinishActivityMessageProducer finishActivityMessageProducer = new FinishActivityMessageProducer( + finishActivityQueueProperty, + FinishActivityMessage.builder() + .activityId(activity.getId()) + .spareTime(spareTime) + .build() + ); + long delayTime = (long) spareTime * 60 * 1000; + finishActivityMessageProducer.publishMessageWithDelay(delayTime); + } +} diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java index a83aba2ad..ce77e14af 100644 --- a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -20,13 +20,14 @@ public class QuickStartActivitySelectService { private final ActivityRepository activityRepository; private final QuickStartRepository quickStartRepository; + private final FinishActivityAutoService finishActivityAutoService; - public QuickStartActivitySelectResponse quickStartUserActivitySelect(Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest - ) { + public QuickStartActivitySelectResponse quickStartUserActivitySelect(Member member, Long quickStartId, QuickStartActivitySelectRequest quickStartActivitySelectRequest) { validateQuickStart(quickStartId); validateRequest(quickStartActivitySelectRequest); Activity activity = Activity.create(member.getId(), quickStartId, quickStartActivitySelectRequest.spareTime(), quickStartActivitySelectRequest.type(), quickStartActivitySelectRequest.keyword(), quickStartActivitySelectRequest.title(), quickStartActivitySelectRequest.content(), quickStartActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); + finishActivityAutoService.finishActivityAuto(savedActivity); return new QuickStartActivitySelectResponse(savedActivity.getId(), savedActivity.getTitle(), savedActivity.getKeyword()); } diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index fce1dc4c8..d5443834c 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -6,7 +6,6 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; -import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.request.UserActivitySelectRequest; import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; @@ -20,10 +19,13 @@ public class UserActivitySelectService { private final ActivityRepository activityRepository; + private final FinishActivityAutoService finishActivityAutoService; + public UserActivitySelectResponse userActivitySelect(Member member, UserActivitySelectRequest userActivitySelectRequest) { validateRequest(userActivitySelectRequest); Activity activity = Activity.create(member.getId(), null, userActivitySelectRequest.spareTime(), userActivitySelectRequest.type(), userActivitySelectRequest.keyword(), userActivitySelectRequest.title(), userActivitySelectRequest.content(), userActivitySelectRequest.location()); Activity savedActivity = activityRepository.save(activity); + finishActivityAutoService.finishActivityAuto(savedActivity); return new UserActivitySelectResponse(savedActivity.getId(), savedActivity.getTitle(), savedActivity.getKeyword()); } diff --git a/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessage.java b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessage.java new file mode 100644 index 000000000..56b927d80 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessage.java @@ -0,0 +1,10 @@ +package spring.backend.activity.infrastructure.queue; + +import lombok.Builder; + +@Builder +public record FinishActivityMessage( + long activityId, + int spareTime +) { +} diff --git a/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageProducer.java b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageProducer.java new file mode 100644 index 000000000..26d67fff0 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageProducer.java @@ -0,0 +1,11 @@ +package spring.backend.activity.infrastructure.queue; + +import spring.backend.core.configuration.property.queue.FinishActivityQueueProperty; +import spring.backend.core.infrastructure.queue.MessageProducer; + +public class FinishActivityMessageProducer extends MessageProducer { + + public FinishActivityMessageProducer(FinishActivityQueueProperty queueProperty, FinishActivityMessage message) { + super(queueProperty, message); + } +} diff --git a/src/main/java/spring/backend/core/configuration/property/queue/FinishActivityQueueProperty.java b/src/main/java/spring/backend/core/configuration/property/queue/FinishActivityQueueProperty.java new file mode 100644 index 000000000..7fdd12d79 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/queue/FinishActivityQueueProperty.java @@ -0,0 +1,10 @@ +package spring.backend.core.configuration.property.queue; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import spring.backend.core.configuration.property.queue.shared.BaseQueueProperty; + +@Component +@ConfigurationProperties("finish-activity-queue") +public class FinishActivityQueueProperty extends BaseQueueProperty { +} From f4e9108b3242c67b5ca4b29379775dd2312b5fab Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 15 Nov 2024 08:59:12 +0900 Subject: [PATCH 277/478] =?UTF-8?q?feat:=20(#78)=20=ED=81=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=99=9C=EB=8F=99=20=EC=A2=85=EB=A3=8C=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EC=86=8C=EB=B9=84=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/entity/ActivityJpaEntity.java | 16 ++++++++- .../queue/FinishActivityMessageConsumer.java | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageConsumer.java diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java index 2b3f25db7..48ff05a78 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java @@ -7,6 +7,7 @@ import lombok.experimental.SuperBuilder; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; +import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; import java.time.LocalDateTime; @@ -42,4 +43,17 @@ public class ActivityJpaEntity extends BaseLongIdEntity { private LocalDateTime finishedAt; private Integer savedTime; -} \ No newline at end of file + + public boolean isFinished() { + return finished != null && finished; + } + + public void finish() { + if (isFinished()) { + throw ActivityErrorCode.ALREADY_FINISHED_ACTIVITY.toException(); + } + finished = true; + finishedAt = LocalDateTime.now(); + savedTime = spareTime; + } +} diff --git a/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageConsumer.java b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageConsumer.java new file mode 100644 index 000000000..cf97284ce --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/queue/FinishActivityMessageConsumer.java @@ -0,0 +1,35 @@ +package spring.backend.activity.infrastructure.queue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.repository.ActivityJpaRepository; +import spring.backend.core.infrastructure.queue.MessageConsumer; + +@Component +@RequiredArgsConstructor +@Transactional +@Log4j2 +public class FinishActivityMessageConsumer implements MessageConsumer { + + private final ActivityJpaRepository activityJpaRepository; + + @Override + @RabbitListener(queues = "${finish-activity-queue.queue}") + public void consumeMessage(FinishActivityMessage message) { + try { + if (message == null) { + log.error("[FinishActivityMessageConsumer] Message is null"); + return; + } + ActivityJpaEntity activity = activityJpaRepository.findById(message.activityId()).orElseThrow(ActivityErrorCode.NOT_EXIST_ACTIVITY::toException); + activity.finish(); + } catch (Exception e) { + log.error("[FinishActivityMessageConsumer] Error processing message", e); + } + } +} From d8e4ad3d06052be00bc02fd124c557736f9bb074 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 02:41:25 +0900 Subject: [PATCH 278/478] =?UTF-8?q?chore:=20(#98)=20build.gradle=EC=97=90?= =?UTF-8?q?=20Sentry=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build.gradle b/build.gradle index ae8b05a1c..e1b02502c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' + id "io.sentry.jvm.gradle" version "4.13.0" } group = 'spring' @@ -13,6 +14,14 @@ java { } } +sentry { + includeSourceContext = true + + org = "hoyeongjun" + projectName = "cnergy-backend" + authToken = System.getenv("SENTRY_AUTH_TOKEN") +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -71,6 +80,8 @@ dependencies { // Netty implementation "io.netty:netty-resolver-dns-native-macos:4.1.113.Final:osx-aarch_64" + // Sentry + implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.17.0' // Email implementation 'org.springframework.boot:spring-boot-starter-mail' From 37ee49d3806246703a93b7095b17738a3ca49795 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 02:42:16 +0900 Subject: [PATCH 279/478] =?UTF-8?q?chore:=20(#98)=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20Sentry=EC=97=90=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../spring/backend/core/exception/GlobalExceptionHandler.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index e1b02502c..df05f3a7a 100644 --- a/build.gradle +++ b/build.gradle @@ -82,6 +82,7 @@ dependencies { // Sentry implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.17.0' + // Email implementation 'org.springframework.boot:spring-boot-starter-mail' diff --git a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java index 0129afb16..3d2d07013 100644 --- a/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java +++ b/src/main/java/spring/backend/core/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package spring.backend.core.exception; +import io.sentry.Sentry; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -21,6 +22,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Exception.class) public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { log.error("ERROR ::: [AllException] ", ex); + Sentry.captureException(ex); ErrorResponse errorResponse = ErrorResponse.createErrorResponse().statusCode(500).exception(ex).build(); return ResponseEntity.internalServerError().body(errorResponse); } @@ -29,6 +31,7 @@ public final ResponseEntity handleAllExceptions(Exception ex, Web public final ResponseEntity handleDomainException(DomainException ex) { HttpStatus httpStatus = Optional.ofNullable(ex.getHttpStatus()).orElse(HttpStatus.INTERNAL_SERVER_ERROR); log.error("ERROR ::: [DomainException] ", ex); + Sentry.captureException(ex); ErrorResponse errorResponse = ErrorResponse.createDomainErrorResponse().statusCode(httpStatus.value()).exception(ex).build(); return ResponseEntity.status(httpStatus).body(errorResponse); } @@ -36,6 +39,7 @@ public final ResponseEntity handleDomainException(DomainException @Override protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { log.error("ERROR ::: [MethodArgumentNotValidException] ", ex); + Sentry.captureException(ex); ErrorResponse errorResponse = ErrorResponse.createValidationErrorResponse().statusCode(400).exception(ex).build(); return ResponseEntity.badRequest().body(errorResponse); } From 9a7031290f3538c8edc46b31acfe3e06068e5e37 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 18 Nov 2024 18:12:45 +0900 Subject: [PATCH 280/478] =?UTF-8?q?fix:=20(#103)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=A2=85=EB=A3=8C=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20Mock=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/QuickStartActivitySelectServiceTest.java | 3 +++ .../activity/application/UserActivitySelectServiceTest.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java index e011603f5..46823d72b 100644 --- a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java @@ -42,6 +42,9 @@ public class QuickStartActivitySelectServiceTest { @Mock private QuickStartRepository quickStartRepository; + @Mock + private FinishActivityAutoService finishActivityAutoService; + private Member member; private QuickStartActivitySelectRequest quickStartActivitySelectRequest; private final Long quickStartId = 1L; diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index 107044f22..14930e4b7 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -32,6 +32,9 @@ public class UserActivitySelectServiceTest { @Mock private ActivityRepository activityRepository; + @Mock + private FinishActivityAutoService finishActivityAutoService; + private Member member; private UserActivitySelectRequest userActivitySelectRequest; From e985938b83a0e60d7ca45f0ee966e79ea593172e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 00:42:28 +0900 Subject: [PATCH 281/478] =?UTF-8?q?fix:=20(#105)=20build.gradle=20?= =?UTF-8?q?=EC=9D=98=20SENTRY=20AUTH=20TOKEN=EC=9D=84=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 --------- 1 file changed, 9 deletions(-) diff --git a/build.gradle b/build.gradle index df05f3a7a..57aeb9774 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' - id "io.sentry.jvm.gradle" version "4.13.0" } group = 'spring' @@ -14,14 +13,6 @@ java { } } -sentry { - includeSourceContext = true - - org = "hoyeongjun" - projectName = "cnergy-backend" - authToken = System.getenv("SENTRY_AUTH_TOKEN") -} - configurations { compileOnly { extendsFrom annotationProcessor From e07252e96c7bd59e45ed84effea4f20c081ea877 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:16:24 +0900 Subject: [PATCH 282/478] =?UTF-8?q?feat:=20(#87)=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=9A=94=EC=95=BD=EC=9D=84=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=EC=9C=84=ED=95=9C=20request=20dto=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/MonthlyActivityOverviewRequest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java diff --git a/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java b/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java new file mode 100644 index 000000000..d700bc0b4 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java @@ -0,0 +1,17 @@ +package spring.backend.activity.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public record MonthlyActivityOverviewRequest( + @NotNull(message = "년도는 필수 입력 값입니다.") + @Min(value = 2024, message = "년도는 2024년 이후 값이어야 합니다.") + int year, + + @NotNull(message = "월은 필수 입력 값입니다.") + @Min(value = 1, message = "월은 1월과 12월 사이 값이어야 합니다.") + @Max(value = 12, message = "월은 1월과 12월 사이 값이어야 합니다.") + int month +) { +} From b87da5b50f05553575fd328e2735ace643518a74 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:20:42 +0900 Subject: [PATCH 283/478] =?UTF-8?q?feat:=20(#87)=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EB=B0=98=ED=99=98=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20response=20dto=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyActivityCountByKeywordResponse.java | 13 +++++++++++++ .../MonthlyActivityOverviewResponse.java | 17 +++++++++++++++++ ...onthlySavedTimeAndActivityCountResponse.java | 12 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java create mode 100644 src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java create mode 100644 src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java new file mode 100644 index 000000000..7f5bef3cf --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java @@ -0,0 +1,13 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.domain.value.Keyword; + +public record MonthlyActivityCountByKeywordResponse( + @Schema(description = "활동의 Keyword" , example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") + Keyword keyword, + + @Schema(description = "Keyword별 활동 횟수" , example = "2") + Long activityCount +) { +} diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java new file mode 100644 index 000000000..9b7ce05aa --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java @@ -0,0 +1,17 @@ +package spring.backend.activity.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record MonthlyActivityOverviewResponse( + @Schema(description = "이번 달 총 모은 자투리 시간(분단위)과 활동횟수") + @JsonProperty("monthlySavedTimeAndActivityCount") + MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCountResponse, + + @Schema(description = "Keyword 별 자투리 시간 및 활동 횟수 요약") + @JsonProperty("activitiesByKeywordSummary") + List activitiesByKeywordSummaryResponses +) { +} diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java new file mode 100644 index 000000000..61a18c0fd --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java @@ -0,0 +1,12 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record MonthlySavedTimeAndActivityCountResponse( + @Schema(description = "이번 달 총 모은 자투리 시간(분단위)", example = "120") + Long monthlyTotalSavedTime, + + @Schema(description = "이번 달 총 활동 횟수", example = "2") + Long monthlyTotalActivityCount +) { +} From 36f3dff056baae3ddc597904807ea9375e24ba4f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:31:33 +0900 Subject: [PATCH 284/478] =?UTF-8?q?feat:=20(#87)=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EB=B0=98=ED=99=98=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20dao=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/ActivityJpaDao.java | 29 +++++++++++++++++++ .../activity/query/dao/ActivityDao.java | 3 ++ 2 files changed, 32 insertions(+) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index ed28ad229..1fa8342e4 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; import spring.backend.activity.dto.response.UserMonthlyActivityDetail; import spring.backend.activity.dto.response.UserMonthlyActivitySummary; @@ -30,6 +31,34 @@ public interface ActivityJpaDao extends JpaRepository, """) List findTodayActivities(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + @Override + @Query(""" + select new spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse( + sum(a.savedTime), + count(a) + ) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + """) + MonthlySavedTimeAndActivityCountResponse findMonthlyTotalSavedTimeAndTotalCount(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + + @Override + @Query(""" + select new spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse( + a.keyword, + count(a) + ) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + group by a.keyword + order by count (a) desc + """) + List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + @Override @Query(""" select new spring.backend.activity.dto.response.UserMonthlyActivitySummary( diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index 90c033d93..8ef7a6846 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -1,5 +1,6 @@ package spring.backend.activity.query.dao; +import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; import spring.backend.activity.dto.response.UserMonthlyActivityDetail; import spring.backend.activity.dto.response.UserMonthlyActivitySummary; @@ -15,4 +16,6 @@ public interface ActivityDao { UserMonthlyActivitySummary findActivitySummaryByYearAndMonth(UUID memberId, int year, int month); List findActivityDetailsByYearAndMonth(UUID memberId, int year, int month); + MonthlySavedTimeAndActivityCountResponse findMonthlyTotalSavedTimeAndTotalCount(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); } From 329eff225b94c1277fbd3bfafc289c64c59e9fb9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:31:55 +0900 Subject: [PATCH 285/478] =?UTF-8?q?feat:=20(#87)=20=ED=8A=B9=EC=A0=95=20?= =?UTF-8?q?=EC=9B=94=EC=9D=98=201=EC=9D=BC=EB=B6=80=ED=84=B0=20=EB=A7=88?= =?UTF-8?q?=EC=A7=80=EB=A7=89=EB=82=A0=EC=9D=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20Util=EC=9D=84=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/util/MonthRangeUtil.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/spring/backend/activity/util/MonthRangeUtil.java diff --git a/src/main/java/spring/backend/activity/util/MonthRangeUtil.java b/src/main/java/spring/backend/activity/util/MonthRangeUtil.java new file mode 100644 index 000000000..30604b3d6 --- /dev/null +++ b/src/main/java/spring/backend/activity/util/MonthRangeUtil.java @@ -0,0 +1,17 @@ +package spring.backend.activity.util; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.time.YearMonth; + +@Getter +public class MonthRangeUtil { + private final LocalDateTime start; + private final LocalDateTime end; + + public MonthRangeUtil(YearMonth yearMonth) { + this.start = yearMonth.atDay(1).atStartOfDay(); + this.end = yearMonth.atEndOfMonth().atTime(23, 59, 59); + } +} From ea582d87e4270cec47798e78e158509586be28a3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:33:10 +0900 Subject: [PATCH 286/478] =?UTF-8?q?feat:=20(#87)=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=9A=94=EC=95=BD=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B3=B5=ED=95=98=EB=8A=94=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadMonthlyActivityOverviewService.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java new file mode 100644 index 000000000..7113d0d44 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -0,0 +1,33 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse; +import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; +import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.activity.util.MonthRangeUtil; +import spring.backend.member.domain.entity.Member; + +import java.time.YearMonth; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Log4j2 +@Transactional(readOnly = true) +public class ReadMonthlyActivityOverviewService { + + private final ActivityDao activityDao; + + public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest) { + YearMonth yearMonth = YearMonth.of(monthlyActivityOverviewRequest.year(), monthlyActivityOverviewRequest.month()); + MonthRangeUtil monthRangeUtil = new MonthRangeUtil(yearMonth); + MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), monthRangeUtil.getStart(), monthRangeUtil.getEnd()); + List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),monthRangeUtil.getStart(), monthRangeUtil.getEnd()); + return new MonthlyActivityOverviewResponse(monthlyActivityOverviewResponse, activityByKeywordSummaryResponses); + } +} From 90b432d66851f72d1fa75356c41c5f108aa5b118 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 15 Nov 2024 17:33:37 +0900 Subject: [PATCH 287/478] =?UTF-8?q?feat:=20(#87)=20=EC=9B=94=EA=B0=84=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=9A=94=EC=95=BD=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B3=B5=ED=95=98=EB=8A=94=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ReadMonthlyActivityOverviewController.java | 32 +++++++++++++++++++ .../ReadMonthlyActivityOverviewSwagger.java | 20 ++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java new file mode 100644 index 000000000..759f19716 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -0,0 +1,32 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.ReadMonthlyActivityOverviewService; +import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.activity.presentation.swagger.ReadMonthlyActivityOverviewSwagger; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class ReadMonthlyActivityOverviewController implements ReadMonthlyActivityOverviewSwagger { + private final ReadMonthlyActivityOverviewService readMonthlyActivityOverviewService; + + @Authorization + @GetMapping("/v1/activity") + public ResponseEntity> readMonthlyActivityOverviewController( + @AuthorizedMember Member member, + @Valid @ModelAttribute MonthlyActivityOverviewRequest monthlyActivityOverviewRequest + ) { + MonthlyActivityOverviewResponse monthlyActivityOverviewResponse = readMonthlyActivityOverviewService.readMonthlyActivityOverview(member, monthlyActivityOverviewRequest); + return ResponseEntity.ok(new RestResponse<>(monthlyActivityOverviewResponse)); + } +} diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java new file mode 100644 index 000000000..ef02acf89 --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java @@ -0,0 +1,20 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "Activity" , description = "활동") +public interface ReadMonthlyActivityOverviewSwagger { + @Operation( + summary = "월간 활동 개요 조회 API", + description = "사용자의 월간 활동 개요를 조회합니다.", + operationId = "/v1/activity" + ) + ResponseEntity> readMonthlyActivityOverviewController(@Parameter(hidden = true) Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest); +} From 90f3f096def2130e4e98e4f8aa771a000ae42dda Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 11:28:39 +0900 Subject: [PATCH 288/478] =?UTF-8?q?refactor:=20(#87)=20Controller=20API=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ReadMonthlyActivityOverviewController.java | 2 +- .../swagger/ReadMonthlyActivityOverviewSwagger.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java index 759f19716..84f30445f 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -22,7 +22,7 @@ public class ReadMonthlyActivityOverviewController implements ReadMonthlyActivit @Authorization @GetMapping("/v1/activity") - public ResponseEntity> readMonthlyActivityOverviewController( + public ResponseEntity> readMonthlyActivityOverview( @AuthorizedMember Member member, @Valid @ModelAttribute MonthlyActivityOverviewRequest monthlyActivityOverviewRequest ) { diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java index ef02acf89..5c9bd53c8 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java @@ -16,5 +16,5 @@ public interface ReadMonthlyActivityOverviewSwagger { description = "사용자의 월간 활동 개요를 조회합니다.", operationId = "/v1/activity" ) - ResponseEntity> readMonthlyActivityOverviewController(@Parameter(hidden = true) Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest); + ResponseEntity> readMonthlyActivityOverview(@Parameter(hidden = true) Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest); } From c908447d90cb4c53c2a4e9a518ae5130d344f9bc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 11:28:53 +0900 Subject: [PATCH 289/478] =?UTF-8?q?refactor:=20(#87)=20Wrapper=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20primitive=20=ED=83=80=EC=9E=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MonthlyActivityCountByKeywordResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java index 7f5bef3cf..b2727e628 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java @@ -8,6 +8,6 @@ public record MonthlyActivityCountByKeywordResponse( Keyword keyword, @Schema(description = "Keyword별 활동 횟수" , example = "2") - Long activityCount + long activityCount ) { } From 188fc62051c6ecc7a97dd0a7e6e561fab44c4c2b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 11:31:20 +0900 Subject: [PATCH 290/478] =?UTF-8?q?refactor:=20(#87)=20api=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EC=9D=98=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ReadMonthlyActivityOverviewController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java index 84f30445f..3c88ba790 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -21,7 +21,7 @@ public class ReadMonthlyActivityOverviewController implements ReadMonthlyActivit private final ReadMonthlyActivityOverviewService readMonthlyActivityOverviewService; @Authorization - @GetMapping("/v1/activity") + @GetMapping("/v1/activities/overview") public ResponseEntity> readMonthlyActivityOverview( @AuthorizedMember Member member, @Valid @ModelAttribute MonthlyActivityOverviewRequest monthlyActivityOverviewRequest From 4a97c8bfbcbeffa1913c6ba35857ff4afe673242 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 11:31:57 +0900 Subject: [PATCH 291/478] =?UTF-8?q?refactor:=20(#87)=20util=EC=97=90=20?= =?UTF-8?q?=EC=9E=88=EB=8D=98=20MonthRange=EB=A5=BC=20converter=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadMonthlyActivityOverviewService.java | 8 ++++---- .../MonthRangeUtil.java => dto/converter/MonthRange.java} | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/spring/backend/activity/{util/MonthRangeUtil.java => dto/converter/MonthRange.java} (71%) diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index 7113d0d44..3106ed07b 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -9,7 +9,7 @@ import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; import spring.backend.activity.query.dao.ActivityDao; -import spring.backend.activity.util.MonthRangeUtil; +import spring.backend.activity.dto.converter.MonthRange; import spring.backend.member.domain.entity.Member; import java.time.YearMonth; @@ -25,9 +25,9 @@ public class ReadMonthlyActivityOverviewService { public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest) { YearMonth yearMonth = YearMonth.of(monthlyActivityOverviewRequest.year(), monthlyActivityOverviewRequest.month()); - MonthRangeUtil monthRangeUtil = new MonthRangeUtil(yearMonth); - MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), monthRangeUtil.getStart(), monthRangeUtil.getEnd()); - List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),monthRangeUtil.getStart(), monthRangeUtil.getEnd()); + MonthRange monthRange = new MonthRange(yearMonth); + MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), monthRange.getStart(), monthRange.getEnd()); + List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),monthRange.getStart(), monthRange.getEnd()); return new MonthlyActivityOverviewResponse(monthlyActivityOverviewResponse, activityByKeywordSummaryResponses); } } diff --git a/src/main/java/spring/backend/activity/util/MonthRangeUtil.java b/src/main/java/spring/backend/activity/dto/converter/MonthRange.java similarity index 71% rename from src/main/java/spring/backend/activity/util/MonthRangeUtil.java rename to src/main/java/spring/backend/activity/dto/converter/MonthRange.java index 30604b3d6..2bd7ccad1 100644 --- a/src/main/java/spring/backend/activity/util/MonthRangeUtil.java +++ b/src/main/java/spring/backend/activity/dto/converter/MonthRange.java @@ -1,4 +1,4 @@ -package spring.backend.activity.util; +package spring.backend.activity.dto.converter; import lombok.Getter; @@ -6,11 +6,11 @@ import java.time.YearMonth; @Getter -public class MonthRangeUtil { +public class MonthRange { private final LocalDateTime start; private final LocalDateTime end; - public MonthRangeUtil(YearMonth yearMonth) { + public MonthRange(YearMonth yearMonth) { this.start = yearMonth.atDay(1).atStartOfDay(); this.end = yearMonth.atEndOfMonth().atTime(23, 59, 59); } From 16a5b8f098c161633c07137c609b14518f9c4e79 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 23:52:20 +0900 Subject: [PATCH 292/478] =?UTF-8?q?refactor:=20(#87)=20MonthRange=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=ED=95=98=EA=B3=A0=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20TimeUtil=EC=97=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/dto/converter/MonthRange.java | 17 ----------------- .../java/spring/backend/core/util/TimeUtil.java | 10 ++++++++++ 2 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/spring/backend/activity/dto/converter/MonthRange.java diff --git a/src/main/java/spring/backend/activity/dto/converter/MonthRange.java b/src/main/java/spring/backend/activity/dto/converter/MonthRange.java deleted file mode 100644 index 2bd7ccad1..000000000 --- a/src/main/java/spring/backend/activity/dto/converter/MonthRange.java +++ /dev/null @@ -1,17 +0,0 @@ -package spring.backend.activity.dto.converter; - -import lombok.Getter; - -import java.time.LocalDateTime; -import java.time.YearMonth; - -@Getter -public class MonthRange { - private final LocalDateTime start; - private final LocalDateTime end; - - public MonthRange(YearMonth yearMonth) { - this.start = yearMonth.atDay(1).atStartOfDay(); - this.end = yearMonth.atEndOfMonth().atTime(23, 59, 59); - } -} diff --git a/src/main/java/spring/backend/core/util/TimeUtil.java b/src/main/java/spring/backend/core/util/TimeUtil.java index a29749cca..7c962e7e6 100644 --- a/src/main/java/spring/backend/core/util/TimeUtil.java +++ b/src/main/java/spring/backend/core/util/TimeUtil.java @@ -4,7 +4,9 @@ import lombok.NoArgsConstructor; import java.time.DateTimeException; +import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.YearMonth; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TimeUtil { @@ -49,4 +51,12 @@ public static Integer toMinute(LocalTime time) { } return time.getMinute(); } + + public static LocalDateTime toStartDayOfMonth(YearMonth yearMonth) { + return yearMonth.atDay(1).atStartOfDay(); + } + + public static LocalDateTime toEndDayOfMonth(YearMonth yearMonth) { + return yearMonth.atEndOfMonth().atTime(23, 59, 59); + } } From 97407aa23350b8e77d941e89d94d35d1129fee4f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 23:52:41 +0900 Subject: [PATCH 293/478] =?UTF-8?q?refactor:=20(#87)=20ModelAttribute?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ReadMonthlyActivityOverviewController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java index 3c88ba790..3d1c2af31 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -24,7 +24,7 @@ public class ReadMonthlyActivityOverviewController implements ReadMonthlyActivit @GetMapping("/v1/activities/overview") public ResponseEntity> readMonthlyActivityOverview( @AuthorizedMember Member member, - @Valid @ModelAttribute MonthlyActivityOverviewRequest monthlyActivityOverviewRequest + @Valid MonthlyActivityOverviewRequest monthlyActivityOverviewRequest ) { MonthlyActivityOverviewResponse monthlyActivityOverviewResponse = readMonthlyActivityOverviewService.readMonthlyActivityOverview(member, monthlyActivityOverviewRequest); return ResponseEntity.ok(new RestResponse<>(monthlyActivityOverviewResponse)); From a2b3aa2bb930b2d412aa30c06c8030ee2afb9cd9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 16 Nov 2024 23:53:14 +0900 Subject: [PATCH 294/478] =?UTF-8?q?refactor:=20(#87)=20Overview=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=9B=94=EB=8F=84=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EC=A4=80=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadMonthlyActivityOverviewService.java | 9 ++++----- .../dto/response/MonthlyActivityOverviewResponse.java | 11 ++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index 3106ed07b..6caec15a6 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -9,7 +9,7 @@ import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; import spring.backend.activity.query.dao.ActivityDao; -import spring.backend.activity.dto.converter.MonthRange; +import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; import java.time.YearMonth; @@ -25,9 +25,8 @@ public class ReadMonthlyActivityOverviewService { public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest) { YearMonth yearMonth = YearMonth.of(monthlyActivityOverviewRequest.year(), monthlyActivityOverviewRequest.month()); - MonthRange monthRange = new MonthRange(yearMonth); - MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), monthRange.getStart(), monthRange.getEnd()); - List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),monthRange.getStart(), monthRange.getEnd()); - return new MonthlyActivityOverviewResponse(monthlyActivityOverviewResponse, activityByKeywordSummaryResponses); + MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), TimeUtil.toStartDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth)); + List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),TimeUtil.toStartDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth)); + return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getMonth(), monthlyActivityOverviewResponse, activityByKeywordSummaryResponses); } } diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java index 9b7ce05aa..018e0987e 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java @@ -1,17 +1,18 @@ package spring.backend.activity.dto.response; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Month; import java.util.List; public record MonthlyActivityOverviewResponse( + @Schema(description = "유저의 가입월") + Month joinedMonth, + @Schema(description = "이번 달 총 모은 자투리 시간(분단위)과 활동횟수") - @JsonProperty("monthlySavedTimeAndActivityCount") - MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCountResponse, + MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCount, @Schema(description = "Keyword 별 자투리 시간 및 활동 횟수 요약") - @JsonProperty("activitiesByKeywordSummary") - List activitiesByKeywordSummaryResponses + List activitiesByKeywordSummary ) { } From ad7d35f22648b8b708fc12b069077c903ebc925b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 14:49:35 +0900 Subject: [PATCH 295/478] =?UTF-8?q?refactor:=20(#87)=20startDayOfMonth,=20?= =?UTF-8?q?endDayOfMonth=EB=A5=BC=20=EB=B3=80=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=BA=80=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadMonthlyActivityOverviewService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index 6caec15a6..50451635e 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -12,6 +12,7 @@ import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; +import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; @@ -25,8 +26,10 @@ public class ReadMonthlyActivityOverviewService { public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest) { YearMonth yearMonth = YearMonth.of(monthlyActivityOverviewRequest.year(), monthlyActivityOverviewRequest.month()); - MonthlySavedTimeAndActivityCountResponse monthlyActivityOverviewResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), TimeUtil.toStartDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth)); - List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(),TimeUtil.toStartDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth)); - return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getMonth(), monthlyActivityOverviewResponse, activityByKeywordSummaryResponses); + LocalDateTime startDayOfMonth = TimeUtil.toStartDayOfMonth(yearMonth); + LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); + MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCountResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), startDayOfMonth, endDayOfMonth); + List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(), startDayOfMonth, endDayOfMonth); + return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getYear() ,member.getUpdatedAt().getMonth(), monthlySavedTimeAndActivityCountResponse, activityByKeywordSummaryResponses); } } From 9a6427ad4d438f81f71c5ed03e0816de28716315 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 14:50:04 +0900 Subject: [PATCH 296/478] =?UTF-8?q?refactor:=20(#87)=20swagger=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/ReadMonthlyActivityOverviewSwagger.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java index 5c9bd53c8..ad997a87f 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java @@ -9,12 +9,12 @@ import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -@Tag(name = "Activity" , description = "활동") +@Tag(name = "Activity", description = "활동") public interface ReadMonthlyActivityOverviewSwagger { @Operation( summary = "월간 활동 개요 조회 API", description = "사용자의 월간 활동 개요를 조회합니다.", - operationId = "/v1/activity" + operationId = "/v1/activities/overview" ) ResponseEntity> readMonthlyActivityOverview(@Parameter(hidden = true) Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest); } From 95964553e38dcbec4beec6c68bcdc1439ed07165 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 14:50:36 +0900 Subject: [PATCH 297/478] =?UTF-8?q?refactor:=20(#87)=20=EC=9B=94=EA=B0=84?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=98=A4=EB=B2=84=EB=B7=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EA=B0=80=EC=9E=85=EB=85=84=EB=8F=84?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EB=B0=98=ED=99=98=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MonthlyActivityOverviewResponse.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java index 018e0987e..5c12ab878 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java @@ -6,7 +6,10 @@ import java.util.List; public record MonthlyActivityOverviewResponse( - @Schema(description = "유저의 가입월") + @Schema(description = "유저의 가입년도", example = "2024") + int joinedYear, + + @Schema(description = "유저의 가입월", example = "JANUARY") Month joinedMonth, @Schema(description = "이번 달 총 모은 자투리 시간(분단위)과 활동횟수") From 0a636cc7f9f0da14f530da6825ac0c5408f73efe Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 14:51:02 +0900 Subject: [PATCH 298/478] =?UTF-8?q?refactor:=20(#87)=20=EC=9B=90=EC=8B=9C?= =?UTF-8?q?=ED=98=95=EC=9D=98=20Null=20=EA=B2=80=EC=82=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/dto/request/MonthlyActivityOverviewRequest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java b/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java index d700bc0b4..3770f0dcb 100644 --- a/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java @@ -2,14 +2,11 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; public record MonthlyActivityOverviewRequest( - @NotNull(message = "년도는 필수 입력 값입니다.") @Min(value = 2024, message = "년도는 2024년 이후 값이어야 합니다.") int year, - @NotNull(message = "월은 필수 입력 값입니다.") @Min(value = 1, message = "월은 1월과 12월 사이 값이어야 합니다.") @Max(value = 12, message = "월은 1월과 12월 사이 값이어야 합니다.") int month From 915e17bf9aaa84b213bf1b2f70ae888a1f7be060 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 14:51:17 +0900 Subject: [PATCH 299/478] =?UTF-8?q?refactor:=20(#87)=20MonthlySavedTimeAnd?= =?UTF-8?q?ActivityCountResponse=EC=9D=98=20=EB=B0=98=ED=99=98=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=9B=90=EC=8B=9C=ED=98=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/MonthlySavedTimeAndActivityCountResponse.java | 4 ++-- .../infrastructure/persistence/jpa/dao/ActivityJpaDao.java | 6 +++--- .../java/spring/backend/activity/query/dao/ActivityDao.java | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java index 61a18c0fd..ff02a316f 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/MonthlySavedTimeAndActivityCountResponse.java @@ -4,9 +4,9 @@ public record MonthlySavedTimeAndActivityCountResponse( @Schema(description = "이번 달 총 모은 자투리 시간(분단위)", example = "120") - Long monthlyTotalSavedTime, + long monthlyTotalSavedTime, @Schema(description = "이번 달 총 활동 횟수", example = "2") - Long monthlyTotalActivityCount + long monthlyTotalActivityCount ) { } diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index 1fa8342e4..38ca4d66d 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -34,8 +34,8 @@ public interface ActivityJpaDao extends JpaRepository, @Override @Query(""" select new spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse( - sum(a.savedTime), - count(a) + coalesce(sum(a.savedTime), 0), + coalesce(count(a), 0) ) from ActivityJpaEntity a where a.memberId = :memberId @@ -48,7 +48,7 @@ public interface ActivityJpaDao extends JpaRepository, @Query(""" select new spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse( a.keyword, - count(a) + coalesce(count(a), 0) ) from ActivityJpaEntity a where a.memberId = :memberId diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index 8ef7a6846..c71a0240c 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -16,6 +16,8 @@ public interface ActivityDao { UserMonthlyActivitySummary findActivitySummaryByYearAndMonth(UUID memberId, int year, int month); List findActivityDetailsByYearAndMonth(UUID memberId, int year, int month); + MonthlySavedTimeAndActivityCountResponse findMonthlyTotalSavedTimeAndTotalCount(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); } From 5fc72c42c36570248f0efa6dd2dbdbecc11561c3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 20:47:04 +0900 Subject: [PATCH 300/478] =?UTF-8?q?refactor:=20(#87)=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EB=82=B4=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadMonthlyActivityOverviewService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index 50451635e..937faa754 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -30,6 +30,6 @@ public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCountResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), startDayOfMonth, endDayOfMonth); List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(), startDayOfMonth, endDayOfMonth); - return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getYear() ,member.getUpdatedAt().getMonth(), monthlySavedTimeAndActivityCountResponse, activityByKeywordSummaryResponses); + return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getYear(), member.getUpdatedAt().getMonth(), monthlySavedTimeAndActivityCountResponse, activityByKeywordSummaryResponses); } } From e72b25037283a43cf83996d5921e968fa3ca5cdc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:30:18 +0900 Subject: [PATCH 301/478] =?UTF-8?q?feat:=20(#102)=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EC=9D=98=20des?= =?UTF-8?q?cription=EC=9C=BC=EB=A1=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20Enum=20=EA=B0=92=EC=9D=84=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=AC=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/activity/domain/value/Keyword.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index dc7e450a2..104e92564 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -5,6 +5,10 @@ import jakarta.persistence.Enumerated; import lombok.*; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -29,6 +33,13 @@ public enum Category { SOCIAL("소셜"); private final String description; + + private static final Map DESCRIPTION_TO_CATEGORY_MAP = Arrays.stream(Category.values()) + .collect(Collectors.toMap(Category::getDescription, category -> category)); + + public static Category from(String description) { + return DESCRIPTION_TO_CATEGORY_MAP.get(description); + } } public static Keyword create(Category category, String image) { From 6135d545550ac79000e0c037cb7896322075d506 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:31:46 +0900 Subject: [PATCH 302/478] =?UTF-8?q?fix:=20(#102)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=98=20description=EC=9D=B4=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20Enum=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20UserMessage?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/recommendation/infrastructure/dto/Message.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java index e9ed7cd5e..317dca1ba 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java @@ -45,11 +45,11 @@ private static String createContent(AIRecommendationRequest aiRecommendationRequ } private static String createContentForActivityTypeOnline(int spareTime, Type activityType, String keywords) { - return String.format("\"자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords); + return String.format("\"자투리 시간: %d분\n선호 활동 타입: %s\n활동 키워드: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType, keywords); } private static String createContentForActivityTypeOfflineOrOnlineAndOffline(int spareTime, Type activityType, String keywords, String location) { - return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType.getDescription(), keywords, location); + return String.format("자투리 시간: %d분\n선호활동: %s\n활동 키워드: %s\n위치: %s\n\n 5가지 활동 추천해줘\n\n", spareTime, activityType, keywords, location); } private static boolean isActivityTypeOfflineOrOnlineAndOffline(Type activityType, String location) { From 1502002ae7d1fe730d77f201dd116d759a3794f9 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:32:31 +0900 Subject: [PATCH 303/478] =?UTF-8?q?feat:=20(#102)=20OpenAI=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openai/dto/request/OpenAIPrompt.java | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java index 50976c3ed..36f091954 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/dto/request/OpenAIPrompt.java @@ -1,15 +1,94 @@ package spring.backend.recommendation.infrastructure.openai.dto.request; public class OpenAIPrompt { - public static final String DEFAULT_SYSTEM_PROMPT = """ - 자투리 시간을 잘 보낼 수 있는 활동을 아래 상황에 맞춰서 구체적으로 추천해줘. - '자기개발'이란 '시사상식, 지식, 교양 측면의 활동들, 지식과 능력을 확장하고 개인의 성장과 발전을 위한 활동'를 말해. - 사용할 수 있는 플랫폼도 같이 알려주면 좋을 거 같아. - 예시 답변은 - 제목 : 최근 흥행작 누구보다 빠르게 찾아보기! - 내용 : 메가박스에서 영화 '대도시의 사랑법' 관람하기 - 플랫폼 : 메가박스 - 이거처럼 작성해주고, 제목은 예시 답변처럼 앞에 형용하는 멋진 문장으로 써줘 - """; + 역할: + 너는 사용자가 입력한 정보를 바탕으로 자투리 시간에 할 수 있는 활동을 추천하는 AI 봇이야. 사용자가 제공하는 정보를 기반으로 적합한 활동을 5가지 추천해줘. 추천은 구체적이고 특정한걸로 되어야하며 추상적이거나 뻔한 활동은 배제해줘. 또한 한국을 기준으로 없는 사이트는 추천하지 말아줘. + --- + 입력 정보: + 1. 자투리 시간: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등). + 2. 활동 타입: + - ONLINE, ONLINE_AND_OFFLINE: 온라인 활동 추천 + 3. 활동 키워드: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등). + 4. 플랫폼: 해당 활동에 사용할 수 있는 온라인 플랫폼 + 5. 링크: 플랫폼의 도메인 주소 + --- + 추천 기준: + 1. 활동 타입이 ONLINE, ONLINE_AND_OFFLINE일 경우: + - 입력된 활동 키워드와 시간을 고려하여 다양한 온라인 활동을 추천. + - 추천되는 활동의 플랫폼은 한국 사이트를 우선순위로 추천. + - 지금 추천에서 콘텐츠라면 특정한 콘텐츠를 지정해서 알려줘. 예를 들어 넷플릭스 다큐라면, 어떤 다큐를 말하는건지, 유튜브 명상 콘텐츠라면 어떤 채널의 콘텐츠인지 등 + --- + 활동 키워드별 정의와 예시: + 1. 자기개발 + - 정의: 시사상식, 지식, 교양과 관련된 활동으로, 개인의 성장과 발전을 위한 것 + - 예시: 뉴스 기사 읽기, 온라인 강연 보기, 팟캐스트 듣기, 언어 공부하기 등 + 2. 엔터테인먼트 + - 정의: 즐거움과 오락을 목적으로 한 활동, 순간의 재미와 유희를 위한 것 + - 예시: 유튜브 콘텐츠 시청하기, 음악듣기, OTT 시청하기 등 + 3. 휴식 + - 정의: 신체적, 정신적 피로 회복과 재충전을 위한 정적인 활동 + - 예시: 명상하기, 짧은 글쓰기, ASMR 듣기 등 + 4. 문화/예술 + - 정의: 예술적, 문화적 경험과 감상을 통해 영감과 인사이트를 얻는 활동 + - 예시: 버추얼 전시 감상하기, 예술 아티클 읽기, 문화예술 영상 보기 등 + 5. 건강 + - 정의: 신체적, 정신적 건강을 개선하고 유지하기 위한 활동, 스포츠 중심 + - 예시: 스트레칭하기, 명상하기, 근력운동하기 등 + 6. 소셜 + - 정의: 사회적 관계 형성과 유지를 위한 활동, 사람들과의 교류와 유대감 + - 예시: SNS 활동하기, 사람들과 소식 공유하기, 사람들에게 연락하기 등 + --- + 출력 형식: + 원하는 활동 타입 == ONLINE, ONLINE_AND_OFFLINE: + - title: [활동 제목 또는 추천장소] + - content: [활동 부제목] + - keyword: [활동 키워드] + - platform: [활동에 사용할 수 있는 온라인 또는 오프라인 플랫폼] + - url: 플랫폼의 도메인 주소 + --- + 예시 입력과 출력: + 예시 (활동 타입 == ONLINE || 활동 타입 == ONLINE_AND_OFFLINE) + 입력: + 자투리 시간: 20분 + 선호 활동 타입: ONLINE + 활동 키워드: 자기개발, 엔터테인먼트, 소셜, 휴식 + + 출력: + title: Daniel Hallak의 TED 강연 듣기 + content: 무궁무진한 세상의 이야기들! + keyword: 자기개발 + platform: TED + url: https://www.ted.com/ + + title: 인사이트 가득한 트렌드 레터 읽기 + content: 요즘 트렌드는 뭐지? + keyword: 자기개발 + platform: 캐릿 + url: https://www.careet.net/ + + title: 유튜브에서 ‘지미 팰런 쇼’ 하이라이트 보기 + content: 미국 코미디 쇼 몰아보기! + keyword: 엔터테인먼트 + platform: Youtube + url: https://www.youtube.com/ + + title: 가장 좋았던 릴스 인스타그램 스토리에 공유하기 + content: 이번주 나의 픽! + keyword: 소셜 + platform: Instagram + url: https://www.instagram.com/ + + title: 유튜브에서 마음을 편안하게 해주는 ASMR 들으며 명상하기 + content: 심신의 안정엔 명상! + keyword: 휴식 + platform: Youtube + url: https://www.youtube.com/ + + 주의사항: + - 활동 타입이 ONLINE이면, 활동을 5개만 추천해줘 + - 활동 타입이 ONLINE_AND_OFFLINE이면, 활동을 2개만 추천해줘 + - 요청으로 location이 있어도 무시하고 추천해줘 + - title, content, keyword, platform, url 구조 이외는 아무런 문장이나 미사여구도 붙이지마 + """; } From e2201288d8b5297d93ccf96c20d348f30d20acc4 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:34:01 +0900 Subject: [PATCH 304/478] =?UTF-8?q?feat:=20(#102)=20OpenAI=20ErrorCode?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openai/exception/OpenAIErrorCode.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java new file mode 100644 index 000000000..af41fdbfa --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java @@ -0,0 +1,25 @@ +package spring.backend.recommendation.infrastructure.openai.exception; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum OpenAIErrorCode implements BaseErrorCode { + + NOT_FOUND_RECOMMENDATION(HttpStatus.NOT_FOUND, "OpenAI에서의 추천이 존재하지 않습니다."), + NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "추천의 카테고리가 존재하지 않습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From 6828c0831a550554288456d734df0ff4dc8be412 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:37:00 +0900 Subject: [PATCH 305/478] =?UTF-8?q?feat:=20(#102)=20OpenAI=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EC=98=A8=EB=9D=BC=EC=9D=B8,=20?= =?UTF-8?q?=EC=98=A8=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EC=9D=84=20=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromOpenAIService.java | 167 ++++++++++++++++++ .../OpenAIRecommendationResponse.java | 20 +++ ...etRecommendationsFromOpenAIController.java | 30 ++++ 3 files changed, 217 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java create mode 100644 src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java create mode 100644 src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java new file mode 100644 index 000000000..1ab7d5bf5 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java @@ -0,0 +1,167 @@ +package spring.backend.recommendation.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import spring.backend.activity.domain.value.Keyword.Category; +import spring.backend.activity.domain.value.Type; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.infrastructure.dto.Message; +import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse; +import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class GetRecommendationsFromOpenAIService { + + private static final String LINE_SEPARATOR = "\n"; + private static final Pattern RECOMMENDATION_FIELD_PATTERN = Pattern.compile("^(title|content|keyword|platform|url):\\s*(.*)$"); + private static final int ONLINE_REQUIRED_SIZE = 5; + private static final int ONLINE_OFFLINE_ONLINE_REQUIRED_SIZE = 2; + private static final String TITLE_KEY = "title"; + private static final String CONTENT_KEY = "content"; + private static final String KEYWORD_KEY = "keyword"; + private static final String PLATFORM_KEY = "platform"; + private static final String URL_KEY = "url"; + + private final Map> FIELD_SETTERS = Map.of( + TITLE_KEY, (key, value) -> title = value, + CONTENT_KEY, (key, value) -> content = value, + KEYWORD_KEY, (key, value) -> keyword = value, + PLATFORM_KEY, (key, value) -> platform = value, + URL_KEY, (key, value) -> url = value + ); + private String title; + private String content; + private String keyword; + private String platform; + private String url; + + private final RecommendationProvider> openAIRecommendationProvider; + + public Mono> getRecommendationsFromOpenAI(AIRecommendationRequest request) { + return openAIRecommendationProvider.getRecommendations(request) + .flatMap(this::extractRecommendationContent) + .switchIfEmpty(Mono.error(OpenAIErrorCode.NOT_FOUND_RECOMMENDATION.toException())) + .flatMap(rawData -> parseRecommendations(rawData, request.activityType())) + .flatMap(recommendations -> checkAndRetryRecommendations(recommendations, request)); + } + + private Mono extractRecommendationContent(OpenAIResponse openAIResponse) { + return Mono.justOrEmpty(openAIResponse) + .map(OpenAIResponse::choices) + .flatMap(choices -> Mono.justOrEmpty(choices.get(0))) + .flatMap(choice -> Mono.justOrEmpty(choice.message())) + .map(Message::content); + } + + private Mono> parseRecommendations(String rawData, Type activityType) { + return Mono.fromCallable(() -> { + String[] lines = rawData.split(LINE_SEPARATOR); + List recommendations = new ArrayList<>(); + int order = 1; + + for (String line : lines) { + Matcher matcher = RECOMMENDATION_FIELD_PATTERN.matcher(line); + if (matcher.matches()) { + String key = matcher.group(1); + String value = matcher.group(2).trim(); + setRecommendationField(key, value); + } + + if (isAllFieldsFilled()) { + Category category = Category.from(keyword); + if (!isValidCategory(category)) { + log.warn("[GetRecommendationsFromOpenAIService] Invalid Category."); + continue; + } + + OpenAIRecommendationResponse response = OpenAIRecommendationResponse.of(order++, title, content, category, url); + recommendations.add(response); + resetRecommendationFields(); + } + } + + return filterAndLimitRecommendations(recommendations, activityType); + }).onErrorResume(e -> { + log.error("[GetRecommendationsFromOpenAIService] Recommendation parsing error: {}", e.getMessage()); + return Mono.error(e); + }); + } + + private List filterAndLimitRecommendations(List recommendations, Type activityType) { + recommendations = filterRecommendations(recommendations); + if (recommendations.isEmpty()) { + throw OpenAIErrorCode.NOT_EXIST_CATEGORY.toException(); + } + return limitRecommendations(recommendations, activityType); + } + + private List filterRecommendations(List recommendations) { + return recommendations.stream() + .filter(r -> r.keywordCategory() != null) + .collect(Collectors.toList()); + } + + private List limitRecommendations(List recommendations, Type activityType) { + int requiredSize = getRequiredSize(activityType); + return recommendations.size() > requiredSize ? recommendations.subList(0, requiredSize) : recommendations; + } + + private Mono> checkAndRetryRecommendations(List recommendations, AIRecommendationRequest request) { + if (isRetryRequired(recommendations, request.activityType())) { + return retryRecommendations(request); + } + return Mono.just(recommendations); + } + + private boolean isRetryRequired(List recommendations, Type activityType) { + return recommendations.isEmpty() || recommendations.size() != getRequiredSize(activityType); + } + + private Mono> retryRecommendations(AIRecommendationRequest request) { + return getRecommendationsFromOpenAI(request); + } + + private int getRequiredSize(Type activityType) { + return switch (activityType) { + case ONLINE -> ONLINE_REQUIRED_SIZE; + case ONLINE_AND_OFFLINE -> ONLINE_OFFLINE_ONLINE_REQUIRED_SIZE; + default -> 0; + }; + } + + private boolean isAllFieldsFilled() { + return title != null && content != null && keyword != null && platform != null && url != null; + } + + private boolean isValidCategory(Category category) { + return category != null; + } + + private void setRecommendationField(String key, String value) { + BiConsumer fieldSetter = FIELD_SETTERS.get(key); + if (fieldSetter != null) { + fieldSetter.accept(key, value); + } + } + + private void resetRecommendationFields() { + title = null; + content = null; + keyword = null; + platform = null; + url = null; + } +} diff --git a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java new file mode 100644 index 000000000..526e4297d --- /dev/null +++ b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java @@ -0,0 +1,20 @@ +package spring.backend.recommendation.dto.response; + +import spring.backend.activity.domain.value.Keyword.Category; + +public record OpenAIRecommendationResponse( + + int order, + + String title, + + String content, + + Category keywordCategory, + + String url +) { + public static OpenAIRecommendationResponse of(int order, String title, String content, Category category, String url) { + return new OpenAIRecommendationResponse(order, title, content, category, url); + } +} diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java new file mode 100644 index 000000000..83a071ae7 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java @@ -0,0 +1,30 @@ +package spring.backend.recommendation.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.recommendation.application.GetRecommendationsFromOpenAIService; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class GetRecommendationsFromOpenAIController { + + private final GetRecommendationsFromOpenAIService getRecommendationsFromOpenAIService; + + @Authorization + @PostMapping("/v1/recommendations/open-ai") + public Mono>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @RequestBody AIRecommendationRequest request) { + return getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request) + .map(RestResponse::new); + } +} From 049911de7f2ab7f048174a6feb13a3fc659e576e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 19 Nov 2024 07:54:19 +0900 Subject: [PATCH 306/478] =?UTF-8?q?feat:=20(#102)=20OpenAI=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20API=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenAIRecommendationResponse.java | 6 +++++ ...etRecommendationsFromOpenAIController.java | 3 ++- .../GetRecommendationsFromOpenAISwagger.java | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java diff --git a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java index 526e4297d..9585003f6 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java @@ -1,17 +1,23 @@ package spring.backend.recommendation.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword.Category; public record OpenAIRecommendationResponse( + @Schema(description = "추천 결과 순서", example = "1") int order, + @Schema(description = "직접적 추천 활동", example = "마음의 편안을 가져다주는 명상 음악 20분 듣기") String title, + @Schema(description = "꾸밈글", example = "휴식에는 역시 명상이 최고!") String content, + @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") Category keywordCategory, + @Schema(description = "외부 링크", example = "https://www.youtube.com") String url ) { public static OpenAIRecommendationResponse of(int order, String title, String content, Category category, String url) { diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java index 83a071ae7..c6c12b281 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java @@ -12,12 +12,13 @@ import spring.backend.recommendation.application.GetRecommendationsFromOpenAIService; import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.presentation.swagger.GetRecommendationsFromOpenAISwagger; import java.util.List; @RestController @RequiredArgsConstructor -public class GetRecommendationsFromOpenAIController { +public class GetRecommendationsFromOpenAIController implements GetRecommendationsFromOpenAISwagger { private final GetRecommendationsFromOpenAIService getRecommendationsFromOpenAIService; diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java new file mode 100644 index 000000000..e4f751a96 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java @@ -0,0 +1,27 @@ +package spring.backend.recommendation.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import reactor.core.publisher.Mono; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; + +import java.util.List; + +@Tag(name = "Recommendation", description = "추천") +public interface GetRecommendationsFromOpenAISwagger { + + @Operation( + summary = "[ONLINE, ONLINE_AND_OFFLINE] 사용자 추천 요청 API", + description = "[ONLINE, ONLINE_AND_OFFLINE] 사용자가 활동 추천을 요청합니다.", + operationId = "/v1/recommendations/open-ai" + ) + @ApiErrorCode({GlobalErrorCode.class, OpenAIErrorCode.class}) + Mono>> GetRecommendationsFromOpenAI(@Parameter(hidden = true) Member member, AIRecommendationRequest request); +} From 74c7df41d6ea477830dde57efe3446dc0399bf51 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 20 Nov 2024 13:20:12 +0900 Subject: [PATCH 307/478] =?UTF-8?q?feat:=20(#102)=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EC=9C=A0=ED=8A=9C?= =?UTF-8?q?=EB=B8=8C=20=EB=B9=84=EB=94=94=EC=98=A4=20URL=EC=9D=84=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/SearchYouTubeService.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/application/SearchYouTubeService.java diff --git a/src/main/java/spring/backend/recommendation/application/SearchYouTubeService.java b/src/main/java/spring/backend/recommendation/application/SearchYouTubeService.java new file mode 100644 index 000000000..a8ab25d54 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/application/SearchYouTubeService.java @@ -0,0 +1,41 @@ +package spring.backend.recommendation.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import spring.backend.recommendation.infrastructure.link.LinkWebClient; +import spring.backend.recommendation.infrastructure.link.youtube.dto.response.YoutubeResponse; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class SearchYouTubeService { + + @Value("${youtube.video-url}") + private String youtubeVideoUrl; + + private final LinkWebClient youtubeLinkWebClient; + + public String searchYoutube(String query) { + if (query == null || query.isEmpty()) { + log.error("[SearchYouTubeService]: query is empty"); + return null; + } + YoutubeResponse youtubeResponse = youtubeLinkWebClient.search(query); + if (youtubeResponse == null || youtubeResponse.items() == null) { + log.error("[SearchYouTubeService]: youtubeResponse is null"); + return null; + } + String videoId = youtubeResponse.items().stream() + .filter(item -> item.id() != null && item.id().videoId() != null) + .map(item -> item.id().videoId()) + .findFirst() + .orElse(null); + if (videoId == null) { + log.error("[SearchYouTubeService]: videoId is empty"); + return null; + } + return youtubeVideoUrl + videoId; + } +} From 4fe6fb6441991d4450cdfb9c06d750d9056dce13 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 20 Nov 2024 13:52:31 +0900 Subject: [PATCH 308/478] =?UTF-8?q?fix:=20(#102)=20OpenAI=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EC=9D=84=20=EC=96=BB=EB=8A=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EB=8F=99=EA=B8=B0=EC=8B=9D=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromOpenAIService.java | 205 +++++++++--------- .../openai/exception/OpenAIErrorCode.java | 2 +- ...etRecommendationsFromOpenAIController.java | 8 +- .../GetRecommendationsFromOpenAISwagger.java | 4 +- 4 files changed, 113 insertions(+), 106 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java index 1ab7d5bf5..a106d19e5 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java @@ -10,12 +10,10 @@ import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; import spring.backend.recommendation.infrastructure.dto.Message; import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse; +import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse.Choice; import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -27,141 +25,150 @@ public class GetRecommendationsFromOpenAIService { private static final String LINE_SEPARATOR = "\n"; private static final Pattern RECOMMENDATION_FIELD_PATTERN = Pattern.compile("^(title|content|keyword|platform|url):\\s*(.*)$"); - private static final int ONLINE_REQUIRED_SIZE = 5; - private static final int ONLINE_OFFLINE_ONLINE_REQUIRED_SIZE = 2; + private static final Pattern LINK_SEARCH_QUERY_PATTERN = Pattern.compile("'(.*?)'"); private static final String TITLE_KEY = "title"; private static final String CONTENT_KEY = "content"; private static final String KEYWORD_KEY = "keyword"; private static final String PLATFORM_KEY = "platform"; private static final String URL_KEY = "url"; - - private final Map> FIELD_SETTERS = Map.of( - TITLE_KEY, (key, value) -> title = value, - CONTENT_KEY, (key, value) -> content = value, - KEYWORD_KEY, (key, value) -> keyword = value, - PLATFORM_KEY, (key, value) -> platform = value, - URL_KEY, (key, value) -> url = value - ); - private String title; - private String content; - private String keyword; - private String platform; - private String url; + private static final String YOUTUBE_PLATFORM = "youtube"; + private static final int ONLINE_REQUIRED_SIZE = 5; + private static final int ONLINE_OFFLINE_OPENAI_REQUIRED_SIZE = 2; + private static final int MAX_RETRY_ATTEMPTS = 2; private final RecommendationProvider> openAIRecommendationProvider; + private final SearchYouTubeService searchYouTubeService; + + public List getRecommendationsFromOpenAI(AIRecommendationRequest request) { + for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { + try { + OpenAIResponse openAIResponse = openAIRecommendationProvider.getRecommendations(request).block(); + if (openAIResponse == null) { + throw OpenAIErrorCode.NOT_FOUND_RECOMMENDATION.toException(); + } - public Mono> getRecommendationsFromOpenAI(AIRecommendationRequest request) { - return openAIRecommendationProvider.getRecommendations(request) - .flatMap(this::extractRecommendationContent) - .switchIfEmpty(Mono.error(OpenAIErrorCode.NOT_FOUND_RECOMMENDATION.toException())) - .flatMap(rawData -> parseRecommendations(rawData, request.activityType())) - .flatMap(recommendations -> checkAndRetryRecommendations(recommendations, request)); - } - - private Mono extractRecommendationContent(OpenAIResponse openAIResponse) { - return Mono.justOrEmpty(openAIResponse) - .map(OpenAIResponse::choices) - .flatMap(choices -> Mono.justOrEmpty(choices.get(0))) - .flatMap(choice -> Mono.justOrEmpty(choice.message())) - .map(Message::content); - } + String rawData = extractRecommendationContent(openAIResponse); + List recommendations = parseRecommendations(rawData, request.activityType()); - private Mono> parseRecommendations(String rawData, Type activityType) { - return Mono.fromCallable(() -> { - String[] lines = rawData.split(LINE_SEPARATOR); - List recommendations = new ArrayList<>(); - int order = 1; - - for (String line : lines) { - Matcher matcher = RECOMMENDATION_FIELD_PATTERN.matcher(line); - if (matcher.matches()) { - String key = matcher.group(1); - String value = matcher.group(2).trim(); - setRecommendationField(key, value); + if (attempt == 1 && (recommendations.isEmpty() || isKeywordMissing(recommendations))) { + log.warn("[GetRecommendationsFromOpenAIService] First attempt failed : {}", recommendations); + continue; } - if (isAllFieldsFilled()) { - Category category = Category.from(keyword); - if (!isValidCategory(category)) { - log.warn("[GetRecommendationsFromOpenAIService] Invalid Category."); - continue; - } + return recommendations; - OpenAIRecommendationResponse response = OpenAIRecommendationResponse.of(order++, title, content, category, url); - recommendations.add(response); - resetRecommendationFields(); + } catch (Exception e) { + if (attempt == MAX_RETRY_ATTEMPTS) { + log.error("[GetRecommendationsFromOpenAIService] Maximum retry attempts exceeded.", e); + throw OpenAIErrorCode.FAILED_RECOMMENDATION_GENERATION.toException(); } + log.warn("[GetRecommendationsFromOpenAIService] Invalid response received. Retrying attempt: {}", attempt + 1, e); } + } + throw OpenAIErrorCode.FAILED_RECOMMENDATION_GENERATION.toException(); + } - return filterAndLimitRecommendations(recommendations, activityType); - }).onErrorResume(e -> { - log.error("[GetRecommendationsFromOpenAIService] Recommendation parsing error: {}", e.getMessage()); - return Mono.error(e); - }); + private String extractRecommendationContent(OpenAIResponse openAIResponse) { + return Optional.ofNullable(openAIResponse) + .map(OpenAIResponse::choices) + .map(choices -> choices.get(0)) + .map(Choice::message) + .map(Message::content) + .orElse(null); } - private List filterAndLimitRecommendations(List recommendations, Type activityType) { - recommendations = filterRecommendations(recommendations); - if (recommendations.isEmpty()) { - throw OpenAIErrorCode.NOT_EXIST_CATEGORY.toException(); + private List parseRecommendations(String rawData, Type activityType) { + if (rawData == null || rawData.isEmpty()) { + throw OpenAIErrorCode.NOT_FOUND_RECOMMENDATION.toException(); } - return limitRecommendations(recommendations, activityType); - } - private List filterRecommendations(List recommendations) { - return recommendations.stream() - .filter(r -> r.keywordCategory() != null) - .collect(Collectors.toList()); - } + String[] lines = rawData.split(LINE_SEPARATOR); + List recommendations = new ArrayList<>(); + int order = 1; - private List limitRecommendations(List recommendations, Type activityType) { - int requiredSize = getRequiredSize(activityType); - return recommendations.size() > requiredSize ? recommendations.subList(0, requiredSize) : recommendations; - } + Map recommendationFields = new HashMap<>(); - private Mono> checkAndRetryRecommendations(List recommendations, AIRecommendationRequest request) { - if (isRetryRequired(recommendations, request.activityType())) { - return retryRecommendations(request); + for (String line : lines) { + Matcher matcher = RECOMMENDATION_FIELD_PATTERN.matcher(line); + + if (matcher.matches()) { + recommendationFields.put(matcher.group(1).toLowerCase(), matcher.group(2).trim()); + } + + if (hasAllRequiredFields(recommendationFields)) { + String keyword = recommendationFields.get(KEYWORD_KEY); + Category category = Category.from(keyword); + + if (isInvalidCategory(category)) { + log.warn("[GetRecommendationsFromOpenAIService] Invalid Category."); + recommendationFields.clear(); + continue; + } + + String title = recommendationFields.get(TITLE_KEY); + String platform = recommendationFields.get(PLATFORM_KEY); + String url = recommendationFields.get(URL_KEY); + String youtubeUrl = processYoutubeUrl(title, platform, url); + + recommendations.add(OpenAIRecommendationResponse.of( + order++, + title, + recommendationFields.get(CONTENT_KEY), + category, + youtubeUrl + )); + + recommendationFields.clear(); + } } - return Mono.just(recommendations); - } - private boolean isRetryRequired(List recommendations, Type activityType) { - return recommendations.isEmpty() || recommendations.size() != getRequiredSize(activityType); + return filterAndLimitRecommendations(recommendations, activityType); } - private Mono> retryRecommendations(AIRecommendationRequest request) { - return getRecommendationsFromOpenAI(request); + private List filterAndLimitRecommendations(List recommendations, Type activityType) { + return recommendations.stream() + .filter(r -> r.keywordCategory() != null) + .limit(getRequiredSize(activityType)) + .collect(Collectors.toList()); } private int getRequiredSize(Type activityType) { return switch (activityType) { case ONLINE -> ONLINE_REQUIRED_SIZE; - case ONLINE_AND_OFFLINE -> ONLINE_OFFLINE_ONLINE_REQUIRED_SIZE; + case ONLINE_AND_OFFLINE -> ONLINE_OFFLINE_OPENAI_REQUIRED_SIZE; default -> 0; }; } - private boolean isAllFieldsFilled() { - return title != null && content != null && keyword != null && platform != null && url != null; + private boolean isInvalidCategory(Category category) { + return category == null; } - private boolean isValidCategory(Category category) { - return category != null; + private boolean isKeywordMissing(List recommendations) { + return recommendations.stream() + .anyMatch(r -> isInvalidCategory(r.keywordCategory())); + } + + private boolean hasAllRequiredFields(Map recommendationFields) { + return recommendationFields.containsKey(TITLE_KEY) + && recommendationFields.containsKey(CONTENT_KEY) + && recommendationFields.containsKey(KEYWORD_KEY) + && recommendationFields.containsKey(PLATFORM_KEY) + && recommendationFields.containsKey(URL_KEY); } - private void setRecommendationField(String key, String value) { - BiConsumer fieldSetter = FIELD_SETTERS.get(key); - if (fieldSetter != null) { - fieldSetter.accept(key, value); + private String processYoutubeUrl(String title, String platform, String url) { + if (YOUTUBE_PLATFORM.equalsIgnoreCase(platform)) { + String youtubeUrl = searchYouTubeService.searchYoutube(extractYoutubeUrlFromTitle(title)); + if (youtubeUrl != null) { + return youtubeUrl; + } } + return url; } - private void resetRecommendationFields() { - title = null; - content = null; - keyword = null; - platform = null; - url = null; + private String extractYoutubeUrlFromTitle(String title) { + Matcher matcher = LINK_SEARCH_QUERY_PATTERN.matcher(title); + return matcher.find() ? matcher.group(1) : null; } } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java index af41fdbfa..fed314d68 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/exception/OpenAIErrorCode.java @@ -12,7 +12,7 @@ public enum OpenAIErrorCode implements BaseErrorCode { NOT_FOUND_RECOMMENDATION(HttpStatus.NOT_FOUND, "OpenAI에서의 추천이 존재하지 않습니다."), - NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "추천의 카테고리가 존재하지 않습니다."); + FAILED_RECOMMENDATION_GENERATION(HttpStatus.INTERNAL_SERVER_ERROR, "추천 생성 중 에러가 발생하였습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java index c6c12b281..94c05c21c 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java @@ -1,10 +1,10 @@ package spring.backend.recommendation.presentation; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Mono; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; @@ -24,8 +24,8 @@ public class GetRecommendationsFromOpenAIController implements GetRecommendation @Authorization @PostMapping("/v1/recommendations/open-ai") - public Mono>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @RequestBody AIRecommendationRequest request) { - return getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request) - .map(RestResponse::new); + public ResponseEntity>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @RequestBody AIRecommendationRequest request) { + List recommendationsFromOpenAI = getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request); + return ResponseEntity.ok(new RestResponse<>(recommendationsFromOpenAI)); } } diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java index e4f751a96..809686605 100644 --- a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import reactor.core.publisher.Mono; +import org.springframework.http.ResponseEntity; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; @@ -23,5 +23,5 @@ public interface GetRecommendationsFromOpenAISwagger { operationId = "/v1/recommendations/open-ai" ) @ApiErrorCode({GlobalErrorCode.class, OpenAIErrorCode.class}) - Mono>> GetRecommendationsFromOpenAI(@Parameter(hidden = true) Member member, AIRecommendationRequest request); + ResponseEntity>> GetRecommendationsFromOpenAI(@Parameter(hidden = true) Member member, AIRecommendationRequest request); } From cc4e981a4e65402de6ac26d9553d3ebf21f0b59f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 20 Nov 2024 22:03:00 +0900 Subject: [PATCH 309/478] =?UTF-8?q?fix:=20(#102)=20OpenAI=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20API=EC=97=90=20=EA=B2=80=EC=A6=9D=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/GetRecommendationsFromOpenAIController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java index 94c05c21c..2bf3c0729 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java @@ -1,5 +1,6 @@ package spring.backend.recommendation.presentation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -24,7 +25,7 @@ public class GetRecommendationsFromOpenAIController implements GetRecommendation @Authorization @PostMapping("/v1/recommendations/open-ai") - public ResponseEntity>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @RequestBody AIRecommendationRequest request) { + public ResponseEntity>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @Valid @RequestBody AIRecommendationRequest request) { List recommendationsFromOpenAI = getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request); return ResponseEntity.ok(new RestResponse<>(recommendationsFromOpenAI)); } From 420c7a21ee5faa1076529bdf854995dbcb8dbb70 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 20 Nov 2024 22:04:49 +0900 Subject: [PATCH 310/478] =?UTF-8?q?feat:=20(#112)=20=EB=B0=B1=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EB=B0=B0=ED=8F=AC=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20CORS=20Origin=EC=97=90=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/configuration/WebMvcConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index 5cfd8d200..17317f28f 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -25,7 +25,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://cnergy.kro.kr") + .allowedOrigins("http://localhost:3000", "https://cnergy.kro.kr", "https://cnergy.p-e.kr") .allowedMethods("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3000); From a0a289e97c79294fe5ca7568f44a9e1cf7de0ca3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 00:46:48 +0900 Subject: [PATCH 311/478] =?UTF-8?q?feat:=20(#95)=20=EC=9D=B4=EB=B2=88=20?= =?UTF-8?q?=EB=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20request=20dt?= =?UTF-8?q?o=EC=99=80=20response=20dto=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tiesByMemberAndKeywordInMonthResponse.java | 21 +++++++++++++++++++ ...ActivityWithTitleAndSavedTimeResponse.java | 19 +++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java create mode 100644 src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java new file mode 100644 index 000000000..6a370a117 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -0,0 +1,21 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.domain.value.Keyword; + +import java.util.List; + +public record ActivitiesByMemberAndKeywordInMonthResponse( + @Schema(description = "이번 달 해당 키워드 활동을 통해 모은 자투리 시간(분단위)", example = "120") + Long totalSavedTimeByKeywordInMonth, + + @Schema(description = "활동 키워드별 활동 목록") + List activities, + + @Schema(description = "활동 키워드별 활동 총 개수") + long totalActivityCountByKeywordInMonth, + + @Schema(description = "키워드 카테고리", example = "NATURE") + Keyword keyword +) { +} diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java new file mode 100644 index 000000000..d656e82ca --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java @@ -0,0 +1,19 @@ +package spring.backend.activity.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +public record ActivityWithTitleAndSavedTimeResponse( + @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") + String title, + + @Schema(description = "모은 시간", example = "60") + int savedTime, + + @Schema(description = "활동 날짜", example = "2021-07-01T00:00:00") + @JsonProperty("dateOfActivity") + LocalDateTime createdAt +) { +} From f424976afe82f9979a88e21af310a9076a13391a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 00:48:36 +0900 Subject: [PATCH 312/478] =?UTF-8?q?feat:=20(#95)=20=EC=9D=B4=EB=B2=88=20?= =?UTF-8?q?=EB=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20DAO=EB=A5=BC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/ActivityJpaDao.java | 39 +++++++++++++++++++ .../activity/query/dao/ActivityDao.java | 4 ++ 2 files changed, 43 insertions(+) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index 38ca4d66d..94d304bbb 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; import spring.backend.activity.dto.response.UserMonthlyActivityDetail; @@ -92,4 +93,42 @@ and function('month', a.createdAt) = :month order by a.createdAt desc """) List findActivityDetailsByYearAndMonth(UUID memberId, int year, int month); + + @Override + @Query(""" + select new spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse( + a.title, + a.savedTime, + a.createdAt + ) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + and a.keyword.category = :keywordCategory + order by a.createdAt DESC + """) + List findActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + + @Override + @Query(""" + select count(a) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + and a.keyword.category = :keywordCategory + """) + long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + + @Override + @Query(""" + select sum(a.savedTime) + from ActivityJpaEntity a + where a.memberId = :memberId + and a.createdAt between :startDateTime and :endDateTime + and a.finished = true + and a.keyword.category = :keywordCategory + """) + Long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index c71a0240c..90b68b963 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -1,5 +1,6 @@ package spring.backend.activity.query.dao; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; import spring.backend.activity.dto.response.UserMonthlyActivityDetail; @@ -20,4 +21,7 @@ public interface ActivityDao { MonthlySavedTimeAndActivityCountResponse findMonthlyTotalSavedTimeAndTotalCount(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + List findActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + Long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } From b319dae860873f0802bd70a618a0f1e8aeb71ef7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 00:49:18 +0900 Subject: [PATCH 313/478] =?UTF-8?q?feat:=20(#95)=20Keyword=20Category?= =?UTF-8?q?=EC=99=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=ED=95=98=EB=8A=94=20mapper=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/KeywordImageMapper.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java new file mode 100644 index 000000000..9a438a491 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java @@ -0,0 +1,21 @@ +package spring.backend.activity.infrastructure.mapper; + +import spring.backend.activity.domain.value.Keyword; + +import java.util.Map; + +public class KeywordImageMapper { + private static final Map CATEGORY_IMAGE_MAP = Map.of( + Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", + Keyword.Category.HEALTH, "images/health.png", + Keyword.Category.NATURE, "images/nature.png", + Keyword.Category.CULTURE_ART, "images/culture_art.png", + Keyword.Category.ENTERTAINMENT, "images/entertainment.png", + Keyword.Category.RELAXATION, "images/relaxation.png", + Keyword.Category.SOCIAL, "images/social.png" + ); + + public static Keyword getImageByCategory(Keyword.Category category) { + return Keyword.create(category, CATEGORY_IMAGE_MAP.get(category)); + } +} From 694fd33d96b78307287744ebfd10c32613db2f24 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 00:49:31 +0900 Subject: [PATCH 314/478] =?UTF-8?q?feat:=20(#95)=20ReadActivitiesByMemberA?= =?UTF-8?q?ndKeywordInMonthService=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...itiesByMemberAndKeywordInMonthService.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java new file mode 100644 index 000000000..099e26105 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -0,0 +1,37 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; +import spring.backend.activity.infrastructure.mapper.KeywordImageMapper; +import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.core.util.TimeUtil; +import spring.backend.member.domain.entity.Member; + +import java.time.YearMonth; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReadActivitiesByMemberAndKeywordInMonthService { + private final ActivityDao activityDao; + + public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member, ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest) { + YearMonth yearMonth = YearMonth.of(activitiesByMemberAndKeywordInMonthRequest.year(), activitiesByMemberAndKeywordInMonthRequest.month()); + List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); + long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); + Long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); + Keyword keyword = KeywordImageMapper.getImageByCategory(activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); + return new ActivitiesByMemberAndKeywordInMonthResponse( + totalSavedTimeByKeywordInMonth, + activities, + countActivitiesByMemberAndKeywordInMonth, + keyword + ); + } +} From 0f34078d54ed894cd0c34b6ef7977b9ede0725cf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 00:49:45 +0900 Subject: [PATCH 315/478] =?UTF-8?q?feat:=20(#95)=20ReadActivitiesByMemberA?= =?UTF-8?q?ndKeywordInMonthController=EB=A5=BC=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...esByMemberAndKeywordInMonthController.java | 33 +++++++++++++++++++ ...itiesByMemberAndKeywordInMonthSwagger.java | 23 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java create mode 100644 src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java new file mode 100644 index 000000000..8f801473d --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -0,0 +1,33 @@ +package spring.backend.activity.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; +import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.activity.presentation.swagger.ReadActivitiesByMemberAndKeywordInMonthSwagger; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +@Validated +public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadActivitiesByMemberAndKeywordInMonthSwagger { + private final ReadActivitiesByMemberAndKeywordInMonthService readActivitiesByMemberAndKeywordInMonthService; + + @Authorization + @GetMapping("/v1/activities/{year}/{month}/keywords/{keywordCategory}") + public ResponseEntity> readActivitiesByMemberAndKeywordInMonth( + @AuthorizedMember Member member, + @Valid ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest + ) { + ActivitiesByMemberAndKeywordInMonthResponse activitiesByMemberAndKeywordInMonthResponse = readActivitiesByMemberAndKeywordInMonthService.readActivitiesByMemberAndKeywordInMonth(member, activitiesByMemberAndKeywordInMonthRequest); + return ResponseEntity.ok(new RestResponse<>(activitiesByMemberAndKeywordInMonthResponse)); + } +} diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java new file mode 100644 index 000000000..206f6216b --- /dev/null +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java @@ -0,0 +1,23 @@ +package spring.backend.activity.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "Activity", description = "활동") +public interface ReadActivitiesByMemberAndKeywordInMonthSwagger { + @Operation( + summary = "이번 달 해당 키워드 활동 조회 API", + description = "이번 달 해당 키워드 활동을 조회합니다.", + operationId = "/v1/activities/{year}/{month}/keywords/{keywordCategory}" + ) + ResponseEntity> readActivitiesByMemberAndKeywordInMonth( + @Parameter(hidden = true) Member member, + ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest + ); +} From 8cffb730567e773941ca4f4f133ed089d30a13d9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 17 Nov 2024 01:05:35 +0900 Subject: [PATCH 316/478] =?UTF-8?q?feat:=20(#95)=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=EB=A5=BC=20Swagger=EC=97=90=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84=ED=95=98=EA=B3=A0,=20PathVariable=EC=9D=84?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...itiesByMemberAndKeywordInMonthService.java | 12 +++++----- ...esByMemberAndKeywordInMonthController.java | 12 +++++++--- ...itiesByMemberAndKeywordInMonthSwagger.java | 24 +++++++++++++++++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index 099e26105..0b5892d5c 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -21,12 +21,12 @@ public class ReadActivitiesByMemberAndKeywordInMonthService { private final ActivityDao activityDao; - public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member, ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest) { - YearMonth yearMonth = YearMonth.of(activitiesByMemberAndKeywordInMonthRequest.year(), activitiesByMemberAndKeywordInMonthRequest.month()); - List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); - long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); - Long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); - Keyword keyword = KeywordImageMapper.getImageByCategory(activitiesByMemberAndKeywordInMonthRequest.keywordCategory()); + public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member,int year, int month, Keyword.Category keywordCategory) { + YearMonth yearMonth = YearMonth.of(year, month); + List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); + long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); + Long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); + Keyword keyword = KeywordImageMapper.getImageByCategory(keywordCategory); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeByKeywordInMonth, activities, diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java index 8f801473d..901ce136f 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -1,12 +1,16 @@ package spring.backend.activity.presentation; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.presentation.swagger.ReadActivitiesByMemberAndKeywordInMonthSwagger; @@ -18,16 +22,18 @@ @RestController @RequiredArgsConstructor @Validated -public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadActivitiesByMemberAndKeywordInMonthSwagger { +public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadActivitiesByMemberAndKeywordInMonthSwagger{ private final ReadActivitiesByMemberAndKeywordInMonthService readActivitiesByMemberAndKeywordInMonthService; @Authorization @GetMapping("/v1/activities/{year}/{month}/keywords/{keywordCategory}") public ResponseEntity> readActivitiesByMemberAndKeywordInMonth( @AuthorizedMember Member member, - @Valid ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest + @PathVariable int year, + @PathVariable int month, + @PathVariable Keyword.Category keywordCategory ) { - ActivitiesByMemberAndKeywordInMonthResponse activitiesByMemberAndKeywordInMonthResponse = readActivitiesByMemberAndKeywordInMonthService.readActivitiesByMemberAndKeywordInMonth(member, activitiesByMemberAndKeywordInMonthRequest); + ActivitiesByMemberAndKeywordInMonthResponse activitiesByMemberAndKeywordInMonthResponse = readActivitiesByMemberAndKeywordInMonthService.readActivitiesByMemberAndKeywordInMonth(member, year, month, keywordCategory); return ResponseEntity.ok(new RestResponse<>(activitiesByMemberAndKeywordInMonthResponse)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java index 206f6216b..ed220b4a7 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java @@ -2,9 +2,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @@ -18,6 +19,25 @@ public interface ReadActivitiesByMemberAndKeywordInMonthSwagger { ) ResponseEntity> readActivitiesByMemberAndKeywordInMonth( @Parameter(hidden = true) Member member, - ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest + @Parameter( + description = "조회할 연도 (2024 이상)", + schema = @Schema(type = "integer", example = "2024", minimum = "2024") + ) + int year, + + @Parameter( + description = "조회할 월 (1~12)", + schema = @Schema(type = "integer", example = "7", minimum = "1", maximum = "12") + ) + int month, + + @Parameter( + description = "키워드 카테고리 (예: NATURE, HEALTH)", + schema = @Schema(type = "string", example = "NATURE", allowableValues = { + "SELF_DEVELOPMENT", "HEALTH", "NATURE", "CULTURE_ART", + "ENTERTAINMENT", "RELAXATION", "SOCIAL" + }) + ) + Keyword.Category keywordCategory ); } From 9fc40ee14cb6bf479e8763ab4c0d48fd82ffd46e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 20:57:25 +0900 Subject: [PATCH 317/478] =?UTF-8?q?refactor:=20(#95)=20=EB=9E=98=ED=8D=BC?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20primitive=20=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...eadActivitiesByMemberAndKeywordInMonthService.java | 11 +++++++---- .../ActivitiesByMemberAndKeywordInMonthResponse.java | 2 +- .../persistence/jpa/dao/ActivityJpaDao.java | 6 +++--- .../backend/activity/query/dao/ActivityDao.java | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index 0b5892d5c..9d2c86eb1 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -12,6 +12,7 @@ import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; +import java.time.LocalDateTime; import java.time.YearMonth; import java.util.List; @@ -21,11 +22,13 @@ public class ReadActivitiesByMemberAndKeywordInMonthService { private final ActivityDao activityDao; - public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member,int year, int month, Keyword.Category keywordCategory) { + public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member, int year, int month, Keyword.Category keywordCategory) { YearMonth yearMonth = YearMonth.of(year, month); - List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); - long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); - Long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), TimeUtil.toFirstDayOfMonth(yearMonth), TimeUtil.toEndDayOfMonth(yearMonth), keywordCategory); + LocalDateTime firstDayOfMonth = TimeUtil.toFirstDayOfMonth(yearMonth); + LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); + List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); + long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); + long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); Keyword keyword = KeywordImageMapper.getImageByCategory(keywordCategory); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeByKeywordInMonth, diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java index 6a370a117..692bbf907 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -7,7 +7,7 @@ public record ActivitiesByMemberAndKeywordInMonthResponse( @Schema(description = "이번 달 해당 키워드 활동을 통해 모은 자투리 시간(분단위)", example = "120") - Long totalSavedTimeByKeywordInMonth, + long totalSavedTimeByKeywordInMonth, @Schema(description = "활동 키워드별 활동 목록") List activities, diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index 94d304bbb..b907dc36a 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -112,7 +112,7 @@ and function('month', a.createdAt) = :month @Override @Query(""" - select count(a) + select coalesce(count(a), 0) from ActivityJpaEntity a where a.memberId = :memberId and a.createdAt between :startDateTime and :endDateTime @@ -123,12 +123,12 @@ select count(a) @Override @Query(""" - select sum(a.savedTime) + select coalesce(sum(a.savedTime), 0) from ActivityJpaEntity a where a.memberId = :memberId and a.createdAt between :startDateTime and :endDateTime and a.finished = true and a.keyword.category = :keywordCategory """) - Long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index 90b68b963..e4d67e88c 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -23,5 +23,5 @@ public interface ActivityDao { List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); List findActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); - Long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } From 2ea4a030cde475e14309af4887bee01e7995b7dc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 19 Nov 2024 22:46:07 +0900 Subject: [PATCH 318/478] =?UTF-8?q?refactor:=20(#95)=20Swagger=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/ActivitiesByMemberAndKeywordInMonthResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java index 692bbf907..7c1825466 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -15,7 +15,7 @@ public record ActivitiesByMemberAndKeywordInMonthResponse( @Schema(description = "활동 키워드별 활동 총 개수") long totalActivityCountByKeywordInMonth, - @Schema(description = "키워드 카테고리", example = "NATURE") + @Schema(description = "키워드", example = "NATURE") Keyword keyword ) { } From cff7f7ec323abd74af280c0266b0b4710f47bd8c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 17:03:21 +0900 Subject: [PATCH 319/478] =?UTF-8?q?refactor:=20(#95)=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadActivitiesByMemberAndKeywordInMonthService.java | 3 +-- .../ReadActivitiesByMemberAndKeywordInMonthController.java | 6 +----- .../java/spring/backend/activity/query/dao/ActivityDao.java | 3 +++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index 9d2c86eb1..60abfcfdc 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; import spring.backend.activity.infrastructure.mapper.KeywordImageMapper; @@ -24,7 +23,7 @@ public class ReadActivitiesByMemberAndKeywordInMonthService { public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member, int year, int month, Keyword.Category keywordCategory) { YearMonth yearMonth = YearMonth.of(year, month); - LocalDateTime firstDayOfMonth = TimeUtil.toFirstDayOfMonth(yearMonth); + LocalDateTime firstDayOfMonth = TimeUtil.toStartDayOfMonth(yearMonth); LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java index 901ce136f..9e1fc093f 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -1,8 +1,5 @@ package spring.backend.activity.presentation; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; @@ -11,7 +8,6 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.presentation.swagger.ReadActivitiesByMemberAndKeywordInMonthSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; @@ -26,7 +22,7 @@ public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadAc private final ReadActivitiesByMemberAndKeywordInMonthService readActivitiesByMemberAndKeywordInMonthService; @Authorization - @GetMapping("/v1/activities/{year}/{month}/keywords/{keywordCategory}") + @GetMapping("/v1/activities/{year}/{month}/keyword/{keywordCategory}") public ResponseEntity> readActivitiesByMemberAndKeywordInMonth( @AuthorizedMember Member member, @PathVariable int year, diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index e4d67e88c..4715a2e03 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -21,7 +21,10 @@ public interface ActivityDao { MonthlySavedTimeAndActivityCountResponse findMonthlyTotalSavedTimeAndTotalCount(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); List findMonthlyActivitiesByKeywordSummary(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime); + List findActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } From 0c8b44b26e6222ddab3402e19510bedba4aab128 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 23:24:03 +0900 Subject: [PATCH 320/478] =?UTF-8?q?refactor:=20(#95)=20ReadActivitiesByMem?= =?UTF-8?q?berAndKeywordInMonthController=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=EB=A5=BC=20PathVariable=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...itiesByMemberAndKeywordInMonthRequest.java | 24 +++++++++++++++ ...esByMemberAndKeywordInMonthController.java | 23 ++++++++------- ...itiesByMemberAndKeywordInMonthSwagger.java | 29 ++++--------------- 3 files changed, 42 insertions(+), 34 deletions(-) create mode 100644 src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java diff --git a/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java b/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java new file mode 100644 index 000000000..9371f3e79 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java @@ -0,0 +1,24 @@ +package spring.backend.activity.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import spring.backend.activity.domain.value.Keyword; + +public record ActivitiesByMemberAndKeywordInMonthRequest( + @Min(value = 2024, message = "년도는 2024년 이후 값이어야 합니다.") + int year, + + @Min(value = 1, message = "월은 1월과 12월 사이 값이어야 합니다.") + @Max(value = 12, message = "월은 1월과 12월 사이 값이어야 합니다.") + int month, + + @NotNull(message = "키워드 카테고리는 필수 값입니다.") + @Schema(description = "키워드 카테고리 (예: NATURE, HEALTH, SELF_DEVELOPMENT, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", example = "NATURE", allowableValues = { + "SELF_DEVELOPMENT", "HEALTH", "NATURE", "CULTURE_ART", + "ENTERTAINMENT", "RELAXATION", "SOCIAL" + }) + Keyword.Category keywordCategory +) { +} diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java index 9e1fc093f..c8979783d 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -1,13 +1,13 @@ package spring.backend.activity.presentation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; -import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.presentation.swagger.ReadActivitiesByMemberAndKeywordInMonthSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; @@ -17,19 +17,22 @@ @RestController @RequiredArgsConstructor -@Validated -public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadActivitiesByMemberAndKeywordInMonthSwagger{ +public class ReadActivitiesByMemberAndKeywordInMonthController implements ReadActivitiesByMemberAndKeywordInMonthSwagger { private final ReadActivitiesByMemberAndKeywordInMonthService readActivitiesByMemberAndKeywordInMonthService; @Authorization - @GetMapping("/v1/activities/{year}/{month}/keyword/{keywordCategory}") + @GetMapping("/v1/activities") public ResponseEntity> readActivitiesByMemberAndKeywordInMonth( @AuthorizedMember Member member, - @PathVariable int year, - @PathVariable int month, - @PathVariable Keyword.Category keywordCategory - ) { - ActivitiesByMemberAndKeywordInMonthResponse activitiesByMemberAndKeywordInMonthResponse = readActivitiesByMemberAndKeywordInMonthService.readActivitiesByMemberAndKeywordInMonth(member, year, month, keywordCategory); + @Valid ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest + ) { + ActivitiesByMemberAndKeywordInMonthResponse activitiesByMemberAndKeywordInMonthResponse = readActivitiesByMemberAndKeywordInMonthService.readActivitiesByMemberAndKeywordInMonth( + member, + activitiesByMemberAndKeywordInMonthRequest.year(), + activitiesByMemberAndKeywordInMonthRequest.month(), + activitiesByMemberAndKeywordInMonthRequest.keywordCategory() + ); + return ResponseEntity.ok(new RestResponse<>(activitiesByMemberAndKeywordInMonthResponse)); } } diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java index ed220b4a7..c0fa93442 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; @@ -13,31 +14,11 @@ @Tag(name = "Activity", description = "활동") public interface ReadActivitiesByMemberAndKeywordInMonthSwagger { @Operation( - summary = "이번 달 해당 키워드 활동 조회 API", - description = "이번 달 해당 키워드 활동을 조회합니다.", - operationId = "/v1/activities/{year}/{month}/keywords/{keywordCategory}" + summary = "특정 달 선택한 키워드 활동 조회 API", + description = "특정 달 선택한 키워드 활동을 조회합니다.", + operationId = "/v1/activities" ) ResponseEntity> readActivitiesByMemberAndKeywordInMonth( - @Parameter(hidden = true) Member member, - @Parameter( - description = "조회할 연도 (2024 이상)", - schema = @Schema(type = "integer", example = "2024", minimum = "2024") - ) - int year, - - @Parameter( - description = "조회할 월 (1~12)", - schema = @Schema(type = "integer", example = "7", minimum = "1", maximum = "12") - ) - int month, - - @Parameter( - description = "키워드 카테고리 (예: NATURE, HEALTH)", - schema = @Schema(type = "string", example = "NATURE", allowableValues = { - "SELF_DEVELOPMENT", "HEALTH", "NATURE", "CULTURE_ART", - "ENTERTAINMENT", "RELAXATION", "SOCIAL" - }) - ) - Keyword.Category keywordCategory + @Parameter(hidden = true) Member member, ActivitiesByMemberAndKeywordInMonthRequest activitiesByMemberAndKeywordInMonthRequest ); } From 717712fa0ce611810aaab8e778f1e6523b3bebda Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 23:25:30 +0900 Subject: [PATCH 321/478] =?UTF-8?q?refactor:=20(#95)=20ActivitiesByMemberA?= =?UTF-8?q?ndKeywordInMonthResponse=EC=9D=98=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ActivitiesByMemberAndKeywordInMonthResponse.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java index 7c1825466..05e6a38bd 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -9,13 +9,13 @@ public record ActivitiesByMemberAndKeywordInMonthResponse( @Schema(description = "이번 달 해당 키워드 활동을 통해 모은 자투리 시간(분단위)", example = "120") long totalSavedTimeByKeywordInMonth, - @Schema(description = "활동 키워드별 활동 목록") - List activities, - @Schema(description = "활동 키워드별 활동 총 개수") long totalActivityCountByKeywordInMonth, - @Schema(description = "키워드", example = "NATURE") + @Schema(description = "활동 키워드별 활동 목록") + List activities, + + @Schema(description = "키워드", example = "{\"category\":\"NATURE\",\"image\":\"https://d1zjcuqflbd5k.cloudfront.net/files/acc_1160/0/1160-2019-07-02-14-07-52-0000.jpg\"}") Keyword keyword ) { } From a3fec58d0952307bc389230f228a74d30f75fbba Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 23:25:52 +0900 Subject: [PATCH 322/478] =?UTF-8?q?refactor:=20(#95)=20ActivityWithTitleAn?= =?UTF-8?q?dSavedTimeResponse=20=EB=B0=98=ED=99=98=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ActivityWithTitleAndSavedTimeResponse.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java index d656e82ca..84de2d5c7 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java @@ -1,6 +1,5 @@ package spring.backend.activity.dto.response; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; @@ -13,7 +12,6 @@ public record ActivityWithTitleAndSavedTimeResponse( int savedTime, @Schema(description = "활동 날짜", example = "2021-07-01T00:00:00") - @JsonProperty("dateOfActivity") - LocalDateTime createdAt + LocalDateTime dateOfActivity ) { } From c3b0d7c4cd32274870bd824b296009b00bbebe0b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 23:26:25 +0900 Subject: [PATCH 323/478] =?UTF-8?q?refactor:=20(#95)=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8A=94=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=A5=BC=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=ED=95=A9=EC=B9=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ivitiesByMemberAndKeywordInMonthService.java | 8 ++++---- ...vedTimeAndActivityCountByKeywordInMonth.java | 12 ++++++++++++ .../persistence/jpa/dao/ActivityJpaDao.java | 17 +++++------------ .../backend/activity/query/dao/ActivityDao.java | 4 +--- 4 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 src/main/java/spring/backend/activity/dto/response/TotalSavedTimeAndActivityCountByKeywordInMonth.java diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index 60abfcfdc..ef64ffe83 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -6,6 +6,7 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; +import spring.backend.activity.dto.response.TotalSavedTimeAndActivityCountByKeywordInMonth; import spring.backend.activity.infrastructure.mapper.KeywordImageMapper; import spring.backend.activity.query.dao.ActivityDao; import spring.backend.core.util.TimeUtil; @@ -26,13 +27,12 @@ public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeyw LocalDateTime firstDayOfMonth = TimeUtil.toStartDayOfMonth(yearMonth); LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - long countActivitiesByMemberAndKeywordInMonth = activityDao.countActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - long totalSavedTimeByKeywordInMonth = activityDao.totalSavedTimeByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); + TotalSavedTimeAndActivityCountByKeywordInMonth totalSavedTimeAndActivityCountByKeywordInMonth = activityDao.findTotalSavedTimeAndActivityCountByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); Keyword keyword = KeywordImageMapper.getImageByCategory(keywordCategory); return new ActivitiesByMemberAndKeywordInMonthResponse( - totalSavedTimeByKeywordInMonth, + totalSavedTimeAndActivityCountByKeywordInMonth.totalSavedTimeByKeywordInMonth(), + totalSavedTimeAndActivityCountByKeywordInMonth.totalActivityCountByKeywordInMonth(), activities, - countActivitiesByMemberAndKeywordInMonth, keyword ); } diff --git a/src/main/java/spring/backend/activity/dto/response/TotalSavedTimeAndActivityCountByKeywordInMonth.java b/src/main/java/spring/backend/activity/dto/response/TotalSavedTimeAndActivityCountByKeywordInMonth.java new file mode 100644 index 000000000..dd1ef6927 --- /dev/null +++ b/src/main/java/spring/backend/activity/dto/response/TotalSavedTimeAndActivityCountByKeywordInMonth.java @@ -0,0 +1,12 @@ +package spring.backend.activity.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record TotalSavedTimeAndActivityCountByKeywordInMonth( + @Schema(description = "이번 달 해당 키워드 활동을 통해 모은 자투리 시간(분단위)", example = "120") + long totalSavedTimeByKeywordInMonth, + + @Schema(description = "활동 키워드별 활동 총 개수", example = "12") + long totalActivityCountByKeywordInMonth +) { +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index b907dc36a..85f928170 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -112,23 +112,16 @@ and function('month', a.createdAt) = :month @Override @Query(""" - select coalesce(count(a), 0) + select new spring.backend.activity.dto.response.TotalSavedTimeAndActivityCountByKeywordInMonth( + coalesce(sum(a.savedTime), 0), + coalesce(count(a), 0) + ) from ActivityJpaEntity a where a.memberId = :memberId and a.createdAt between :startDateTime and :endDateTime and a.finished = true and a.keyword.category = :keywordCategory """) - long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + TotalSavedTimeAndActivityCountByKeywordInMonth findTotalSavedTimeAndActivityCountByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); - @Override - @Query(""" - select coalesce(sum(a.savedTime), 0) - from ActivityJpaEntity a - where a.memberId = :memberId - and a.createdAt between :startDateTime and :endDateTime - and a.finished = true - and a.keyword.category = :keywordCategory - """) - long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index 4715a2e03..5fbf99fb5 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -24,7 +24,5 @@ public interface ActivityDao { List findActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); - long countActivitiesByMemberAndKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); - - long totalSavedTimeByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); + TotalSavedTimeAndActivityCountByKeywordInMonth findTotalSavedTimeAndActivityCountByKeywordInMonth(UUID memberId, LocalDateTime startDateTime, LocalDateTime endDateTime, Keyword.Category keywordCategory); } From 4dd794cc6be4bc03b43c967f245226a6a6e35d6a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 21 Nov 2024 16:34:17 +0900 Subject: [PATCH 324/478] =?UTF-8?q?refactor:=20(#95)=20keywordImageMapper?= =?UTF-8?q?=EB=A5=BC=20Keyword=20=EC=95=88=EC=97=90=20=EC=8A=A4=ED=83=9C?= =?UTF-8?q?=ED=8B=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...itiesByMemberAndKeywordInMonthService.java | 3 +-- .../activity/domain/value/Keyword.java | 16 ++++++++++++++ .../mapper/KeywordImageMapper.java | 21 ------------------- 3 files changed, 17 insertions(+), 23 deletions(-) delete mode 100644 src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index ef64ffe83..cf16b1248 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -7,7 +7,6 @@ import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; import spring.backend.activity.dto.response.TotalSavedTimeAndActivityCountByKeywordInMonth; -import spring.backend.activity.infrastructure.mapper.KeywordImageMapper; import spring.backend.activity.query.dao.ActivityDao; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; @@ -28,7 +27,7 @@ public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeyw LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); TotalSavedTimeAndActivityCountByKeywordInMonth totalSavedTimeAndActivityCountByKeywordInMonth = activityDao.findTotalSavedTimeAndActivityCountByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - Keyword keyword = KeywordImageMapper.getImageByCategory(keywordCategory); + Keyword keyword = Keyword.getKeywordByCategory(keywordCategory); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeAndActivityCountByKeywordInMonth.totalSavedTimeByKeywordInMonth(), totalSavedTimeAndActivityCountByKeywordInMonth.totalActivityCountByKeywordInMonth(), diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 104e92564..37990ec64 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -45,4 +45,20 @@ public static Category from(String description) { public static Keyword create(Category category, String image) { return new Keyword(category, image); } + + public static Keyword getKeywordByCategory(Category category) { + return Keyword.create(category, getCategoryImageMap().get(category)); + } + + private static Map getCategoryImageMap() { + return Map.of( + Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", + Keyword.Category.HEALTH, "images/health.png", + Keyword.Category.NATURE, "images/nature.png", + Keyword.Category.CULTURE_ART, "images/culture_art.png", + Keyword.Category.ENTERTAINMENT, "images/entertainment.png", + Keyword.Category.RELAXATION, "images/relaxation.png", + Keyword.Category.SOCIAL, "images/social.png" + ); + } } diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java deleted file mode 100644 index 9a438a491..000000000 --- a/src/main/java/spring/backend/activity/infrastructure/mapper/KeywordImageMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package spring.backend.activity.infrastructure.mapper; - -import spring.backend.activity.domain.value.Keyword; - -import java.util.Map; - -public class KeywordImageMapper { - private static final Map CATEGORY_IMAGE_MAP = Map.of( - Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", - Keyword.Category.HEALTH, "images/health.png", - Keyword.Category.NATURE, "images/nature.png", - Keyword.Category.CULTURE_ART, "images/culture_art.png", - Keyword.Category.ENTERTAINMENT, "images/entertainment.png", - Keyword.Category.RELAXATION, "images/relaxation.png", - Keyword.Category.SOCIAL, "images/social.png" - ); - - public static Keyword getImageByCategory(Keyword.Category category) { - return Keyword.create(category, CATEGORY_IMAGE_MAP.get(category)); - } -} From e654073123046af9a6805b8f87af98dc0335ee1f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:46:44 +0900 Subject: [PATCH 325/478] =?UTF-8?q?refactor:=20(#99)=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=20=EC=8A=A4=ED=8A=9C=EB=94=94=EC=98=A4=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/ClovaStudioPrompt.java | 204 +++++++----------- 1 file changed, 75 insertions(+), 129 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index 7e43d626a..74828d25a 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -1,162 +1,108 @@ package spring.backend.recommendation.infrastructure.clova.dto.request; public class ClovaStudioPrompt { - public static final String DEFAULT_SYSTEM_PROMPT = "너는 사용자에게 자투리 시간 , 원하는 활동 타입(ONLINE, OFFLINE, ONLINE_AND_OFFLINE), 하고싶은 활동의 주제, 원하는 활동 타입이 OFFLINE 또는 ONLINE_AND_OFFLINE의 경우 위치를 입력받은 뒤 입력받은 값들을 고려해 5가지 활동을 추천하는 봇이야.\n" + - " 원하는 활동 타입이 ONLINE인 경우, 하고싶은 활동의 주제에 맞는 아티클, 동영상(Youtube), 신문기사, 블로그 글 등을 링크와 함께 5가지 추천해줘.\n" + - " 원하는 활동 타입이 OFFLINE 또는 ONLINE_AND_OFFLINE인 경우, 하고싶은 활동의 주제와 현재 위치를 고려해 현재 위치 주변의 활동 또는 장소를 주소와 함께 5가지 추천해줘.\n" + + public static final String DEFAULT_SYSTEM_PROMPT = "**역할:**\n" + "\n" + - "---\n" + - "\n" + - " 답변 형식 : \n" + - "\n" + - "원하는 활동 타입 == ONLINE:\n" + - "\n" + - " title: [활동 제목 + 링크]\n" + - " content: [활동 부제목]\n" + - " keyword: [활동 주제]\n" + - "\n" + - "원하는 활동 타입 == OFFLINE:\n" + - "\n" + - " title: [활동 제목 또는 추천장소 + 네이버 맵 링크]\n" + - " content: [활동 부제목]\n" + - " keyword: [활동 주제]\n" + - "\n" + - "원하는 활동 타입 == ONLINE_AND_OFFLINE\n" + - "\n" + - " title: [활동 제목 + 링크]\n" + - " content: [활동 부제목]\n" + - " keyword: [활동 주제]\n" + - "\n" + - " title: [활동 제목 또는 추천장소 + 네이버 맵 링크]\n" + - " content: [활동 부제목]\n" + - " keyword: [활동 주제]\n" + + "너는 사용자가 입력한 정보를 바탕으로 자투리 시간에 할 수 있는 활동을 추천하는 AI 봇이야. 사용자가 제공하는 정보를 기반으로 적합한 활동을 5가지 추천해줘.\n" + "\n" + "---\n" + "\n" + - "예상 시나리오 (선호활동 == ONLINE)\n" + - "\n" + - " 질문 예시\n" + - "\n" + - "“””\n" + - "\n" + - "자투리 시간: 10분\n" + - "선호활동: ONLINE\n" + - "활동 키워드: 휴식\n" + + "**입력 정보:**\n" + "\n" + - "활동 추천해줘\n" + + "1. **자투리 시간**: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등).\n" + + "2. **활동 타입**:\n" + + " - `OFFLINE`: 오프라인 활동 추천\n" + + "3. **활동 키워드**: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등).\n" + + "4. **위치**: 사용자 위치 정보.\n" + "\n" + - "“””\n" + - "\n" + - " 답변 예시\n" + - "\n" + - "“””\n" + + "---\n" + "\n" + - "title: 스트리밍 서비스에서 편안한 재즈 음악 듣기\n" + - "https://www.youtube.com/watch?v=Dx5qFachd3A\n" + - "content: 편안한 음악에 귀 기울여보세요!\n" + - "keyword: 휴식\n" + + "**추천 기준:**\n" + "\n" + - "title: 유튜브에서 ASMR 영상 감상하기 https://youtu.be/km-f0NKRve4?si=mC-KYJMTnqT_jOKX\n" + - "content: 편안한 분위기로 마음을 가다듬어 보세요! \n" + - "keyword: 휴식\n" + + "1. **활동 타입이 `OFFLINE`일 경우:**\n" + + " - 입력된 활동 키워드와 위치를 고려하여 근처의 특정한 활동 장소를 주소와 함께 추천.\n" + "\n" + - "title: 유튜브에서 10CM 차분한 노래 라이브 영상 보기 https://youtu.be/JtoU_D282L8?si=PfMyImXYPNSz6DTj\n" + - "content: 눈을 감고 음악에 몸을 맡겨보세요! \n" + - "keyword: 휴식\n" + + "---\n" + "\n" + - "title: 온라인 명상 앱 사용하기 https://play.google.com/store/apps/details?id=app.meditasyon&hl=ko\n" + - "content: 마음의 여유를 느껴보세요! \n" + - "keyword: 휴식\n" + + "**활동 키워드별 정의와 예시:**\n" + + "\n" + + "1. **SELF_DEVELOPMENT**\n" + + " - **정의**: 시사상식, 지식, 교양과 관련된 활동으로, 개인의 성장과 발전을 위한 것\n" + + " - **예시:** 서점 및 도서관 방문하여 책 읽기, 박물관 방문하기 등\n" + + "2. **ENTERTAINMENT**\n" + + " - **정의**: 즐거움과 오락을 목적으로 한 활동, 순간의 재미와 유희를 위한 것\n" + + " - **예시**: 쇼핑하기, 노래방 가기, 영화보기, 볼링치기 등\n" + + "3. **RELAXATION**\n" + + " - **정의**: 신체적, 정신적 피로 회복과 재충전을 위한 정적인 활동\n" + + " - **예시**: 찻집 가기, 공원 걷기, 자연 감상하기 등\n" + + "4. **CULTURE_ART**\n" + + " - **정의**: 예술적, 문화적 경험과 감상을 통해 영감과 인사이트를 얻는 활동\n" + + " - **예시**: 전시회 관람하기, 갤러리 카페 방문하기, 미술관 방문하기 등\n" + + "5. **HEALTH**\n" + + " - **정의**: 신체적, 정신적 건강을 개선하고 유지하기 위한 활동, 스포츠 중심\n" + + " - **예시**: 공원에서 러닝하기, 스트레칭하기, 각종 스포츠 활동하기 등\n" + + "6. **NATURE**\n" + + " - **정의**: 자연과의 접촉을 통해 휴식과 치유를 얻는 활동\n" + + " - **예시**: 자연 감상하기, 근처 공원이나 산 둘러보기, 식물원 및 수목원 방문하기 등\n" + "\n" + - "title: 클래식 음악 감상하기\n" + - "https://www.youtube.com/live/ZRuE2W7R5O8?si=PwPe0qVaMukLTV0A\n" + - "content: 음악의 세계로 빠져보세요! \n" + - "keyword: 휴식\n" + + "---\n" + "\n" + - "“””\n" + + "**출력 형식:**\n" + "\n" + - "예상 시나리오 (선호활동 == OFFLINE)\n" + + "### 원하는 활동 타입 == OFFLINE:\n" + "\n" + - " 질문 예시\n" + + "- title: [활동 제목 또는 추천장소]\n" + + "- placeName: [활동 장소 또는 추천장소의 이름]\n" + + "- content: [활동 부제목]\n" + + "- keyword: [활동 키워드]\n" + "\n" + - "“””\n" + + "---\n" + "\n" + - "자투리 시간: 60분\n" + - "선호활동: OFFLINE\n" + - "위치: 서울특별시 중구 명동\n" + - "활동 키워드: 자기개발 , 문화/예술\n" + + "**예시 입력과 출력:**\n" + "\n" + - "활동 추천해줘\n" + + "### 예시 (활동 타입 == OFFLINE)\n" + "\n" + - "“””\n" + + "**입력:**\n" + "\n" + - "답변 예시\n" + + "- 자투리 시간: 30분\n" + + "- 선호 활동 타입: OFFLINE\n" + + "- 위치: 서울특별시 중구 명동\n" + + "- 활동 키워드: SELF_DEVELOPMENT, CULTURE_ART, ENTERTAINMENT\n" + "\n" + - "“””\n" + + "**출력:**\n" + "\n" + - "title: 서울도서관에서 책 읽기 https://naver.me/5GyhoBuH\n" + + "title: 서울도서관에서 인사이트 가득한 책 읽기\n" + + "placeName: 서울도서관\n" + "content: 독서는 마음의 양식!\n" + - "keyword: 자기개발\n" + - "\n" + - "title: 청운문학도서관에서 책 읽기 https://naver.me/G38LxMfy\n" + - "content: 독서에 예쁜 풍경은 덤!\n" + - "keyword: 자기개발\n" + - "\n" + - "title: 현대미술을 만나는 공간, 국립현대미술관 방문하기 https://naver.me/54Vkke2z\n" + - "content: 미술작품을 보며 미술과 더 친해져봐요!\n" + - "keyword: 문화/예술\n" + - "\n" + - "title: 다양한 예술을 한자리에서, 서울시립미술관 방문하기 https://naver.me/FT0kXrVZ\n" + - "content: 근처에 이런 멋진 곳이!\n" + - "keyword: 문화/예술\n" + - "\n" + - "title: 사진 예술의 매력, 뮤지엄한미 삼청별관 방문하기 https://naver.me/GsTWIOB2\n" + - "content: 근처에 이런 멋진 곳이!\n" + - "keyword: 문화/예술\n" + - "\n" + - "“””\n" + - "\n" + - "예상 시나리오 (선호활동 == ONLINE_AND_OFFLINE)\n" + + "keyword: SELF_DEVELOPMENT\n" + "\n" + - " 질문 예시\n" + + "title: 현대미술 작품을 만날 수 있는 국립현대미술관 방문하기 \n" + + "placeName: 국립현대미술관\n" + + "content: 예술과 가까워지는 시간! \n" + + "keyword: CULTURE_ART\n" + "\n" + - "“””\n" + + "title: 그라운드시소 명동에서 지금 핫한 전시 관람하기 \n" + + "placeName: 그라운드시소 명동\n" + + "content: 도심 속 예술 전시! \n" + + "keyword: CULTURE_ART\n" + "\n" + - "자투리 시간: 60분\n" + - "선호활동: OFFLINE\n" + - "위치: 서울특별시 중구 명동\n" + - "활동 키워드: 자기개발 , 문화/예술, 엔터테인먼트\n" + + "title: 명동 거리에서 최신 패션 아이템 구경하며 쇼핑하기 \n" + + "placeName: 명동거리\n" + + "content: 명동에서 아이 쇼핑!\n" + + "keyword: SELF_DEVELOPMENT\n" + "\n" + - "활동 추천해줘\n" + + "title: 팝마트 명동 프리미엄 테마샵에서 피규어와 장난감 구경하기 \n" + + "placeName: 팝마트 명동 프리미엄 테마샵\n" + + "content: 동심으로 돌아가는 시간! \n" + + "keyword: SELF_DEVELOPMENT\n" + "\n" + - "“””\n" + - "\n" + - "답변 예시\n" + - "\n" + - "“””\n" + - "\n" + - "title: 서울도서관에서 책 읽기 https://naver.me/5GyhoBuH\n" + - "content: 독서는 마음의 양식!\n" + - "keyword: 자기개발\n" + - "\n" + - "title: 청운문학도서관에서 책 읽기 https://naver.me/G38LxMfy\n" + - "content: 독서에 예쁜 풍경은 덤!\n" + - "keyword: 자기개발\n" + - "\n" + - "title: TVING에서 밀린 드라마 에피소드 한 편 정주행 [https://www.tving.com/?utm_source=google&utm_medium=searchad&utm_campaign=PM_google_sa_conv&utm_content=brand_non&utm_term=티빙&gad_source=1&gclid=Cj0KCQjw4Oe4BhCcARIsADQ0csnOzs8W_Rnfqt5gDppg1QHBl5G7tUUddD4FyiwrMtX2PBee3vb6G5EaAnwyEALw_wcB](https://www.tving.com/?utm_source=google&utm_medium=searchad&utm_campaign=PM_google_sa_conv&utm_content=brand_non&utm_term=%ED%8B%B0%EB%B9%99&gad_source=1&gclid=Cj0KCQjw4Oe4BhCcARIsADQ0csnOzs8W_Rnfqt5gDppg1QHBl5G7tUUddD4FyiwrMtX2PBee3vb6G5EaAnwyEALw_wcB)\n" + - "content: 감동을 선사하는 몰입의 시간! \n" + - "keyword: 엔터테인먼트\n" + - "\n" + - "title: 넷플릭스에서 한 배우의 작품 세계에 푹 빠져보는 시간을 가지 https://www.netflix.com/browse\n" + - "content: 좋아하는 배우의 필모그래피 정복하기! \n" + - "keyword: 엔터테인먼트\n" + + "---\n" + "\n" + - "title: 흥미로운 팟캐스트 청취하기 https://www.podbbang.com/\n" + - "content: 유익한 정보를 쌓아보세요! \n" + - "keyword: 엔터테인먼트\n" + + "**유의사항:**\n" + "\n" + - "“”” " + - "유의사항 : \n" + - "- 답변의 title에 link는 반드시 title과 같은 줄에 반환합니다.\n" + - "- 답변의 keyword는 반드시 한 개입니다."; + "1. `keyword`는 반드시 한 개만 반환합니다.\n" + + "2. `placeName` 은 반드시 활동 장소 또는 추천 장소의 이름(명사)의 형태로 제공합니다.\n" + + "3. 추천은 5개여야 합니다.\n" + + "4. 활동 소요 시간을 기준으로 추천 활동을 설계. spareTime으로 주어진 시간을 알차게 활용할 수 있는 활동만 제공합니다.\n" + + "5. `title`, `placeName`, `content`, `keyword`의 구조를 유지하면서도 내용이 중복되지 않도록 세부 사항을 차별화합니다."; } From e3687330451a3558329f589b1754fedd91da779a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:47:47 +0900 Subject: [PATCH 326/478] =?UTF-8?q?refactor:=20(#99)=20PlaceInfoProvider?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=ED=99=98=ED=98=95=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EB=84=A4=EB=A6=AD=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recommendation/application/PlaceInfoProvider.java | 6 ++---- .../map/naver/application/NaverPlaceInfoProvider.java | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java b/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java index c651e473e..d674c4920 100644 --- a/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java +++ b/src/main/java/spring/backend/recommendation/application/PlaceInfoProvider.java @@ -1,7 +1,5 @@ package spring.backend.recommendation.application; -import spring.backend.recommendation.infrastructure.map.naver.dto.response.NaverMapResponse; - -public interface PlaceInfoProvider { - NaverMapResponse search(String query); +public interface PlaceInfoProvider { + T search(String query); } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java index 672578d19..b6786c34e 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/naver/application/NaverPlaceInfoProvider.java @@ -21,7 +21,7 @@ @Component @Log4j2 @RequiredArgsConstructor -public class NaverPlaceInfoProvider implements PlaceInfoProvider { +public class NaverPlaceInfoProvider implements PlaceInfoProvider { @Value("${naver.client-id}") private String clientId; From 284b589f2fad2bd5d13c3e2eb38fabe166d3ecf9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:48:32 +0900 Subject: [PATCH 327/478] =?UTF-8?q?feat:=20(#99)=20KakaoPlaceInfoProvider?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/KakaoPlaceInfoProvider.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java new file mode 100644 index 000000000..8a9d2c7fd --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java @@ -0,0 +1,51 @@ +package spring.backend.recommendation.infrastructure.map.kakao.application; + +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import spring.backend.recommendation.application.PlaceInfoProvider; +import spring.backend.recommendation.infrastructure.map.kakao.dto.response.KakaoMapResponse; +import spring.backend.recommendation.infrastructure.map.kakao.exception.KakaoMapErrorCode; + +@Component +@Log4j2 +public class KakaoPlaceInfoProvider implements PlaceInfoProvider { + + @Value("${kakao.rest-api-key}") + private String restApiKey; + + private static final String BASE_URL = "https://dapi.kakao.com"; + private static final String SEARCH_PATH = "/v2/local/search/keyword.json"; + private static final String QUERY = "query"; + private static final String AUTHORIZATION = "Authorization"; + private static final String KAKAO_AK = "KakaoAK "; + + private final WebClient webClient; + + public KakaoPlaceInfoProvider() { + this.webClient = WebClient.create(BASE_URL); + } + + @Override + public KakaoMapResponse search(String query) { + try { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path(SEARCH_PATH) + .queryParam(QUERY, query) + .build()) + .header(AUTHORIZATION, KAKAO_AK + restApiKey) + .retrieve() + .bodyToMono(KakaoMapResponse.class) + .block(); + } catch (WebClientResponseException e) { + log.error("카카오 지도 API 응답에 오류가 발생했습니다. 상태 코드: {} , 응답 본문: {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw KakaoMapErrorCode.RESPONSE_ERROR.toException(); + } catch (Exception e) { + log.error("장소 검색 중 예상치 못한 오류가 발생했습니다.", e); + throw KakaoMapErrorCode.UNKNOWN_ERROR.toException(); + } + } +} From 5b3897eb427458c9b5fec58ae53478a65c178305 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:48:49 +0900 Subject: [PATCH 328/478] =?UTF-8?q?feat:=20(#99)=20KakaoMapResponse=20dto?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kakao/dto/response/KakaoMapResponse.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java new file mode 100644 index 000000000..fa70edfdc --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java @@ -0,0 +1,38 @@ +package spring.backend.recommendation.infrastructure.map.kakao.dto.response; + +import java.util.List; + +public record KakaoMapResponse( + List documents, + Meta meta +) { + public record Document( + String address_name, + String category_group_code, + String category_group_name, + String category_name, + String distance, + String id, + String phone, + String place_name, + String place_url, + String road_address_name, + String x, + String y + ) { + } + + public record Meta( + boolean is_end, + int pageable_count, + int total_count, + SameName sameName + ) { + public record SameName( + List region, + List keyword, + List selected_region + ) { + } + } +} From 64fc589ce02253850908755db75804c6a4337e54 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:49:02 +0900 Subject: [PATCH 329/478] =?UTF-8?q?feat:=20(#99)=20KakaoMapErrorCode?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kakao/exception/KakaoMapErrorCode.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/spring/backend/recommendation/infrastructure/map/kakao/exception/KakaoMapErrorCode.java diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/exception/KakaoMapErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/exception/KakaoMapErrorCode.java new file mode 100644 index 000000000..ba847cdea --- /dev/null +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/exception/KakaoMapErrorCode.java @@ -0,0 +1,23 @@ +package spring.backend.recommendation.infrastructure.map.kakao.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import spring.backend.core.exception.DomainException; +import spring.backend.core.exception.error.BaseErrorCode; + +@Getter +@RequiredArgsConstructor +public enum KakaoMapErrorCode implements BaseErrorCode { + RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 지도 API 응답에 오류가 발생했습니다."), + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 지도 API 요청 중 알 수 없는 오류가 발생했습니다."); + + private final HttpStatus httpStatus; + + private final String message; + + @Override + public DomainException toException() { + return new DomainException(httpStatus, this); + } +} From e777ee725d3f25a04a98e4b30a7fb1cf19ad5392 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:49:42 +0900 Subject: [PATCH 330/478] =?UTF-8?q?refactor:=20(#99)=20ClovaRecommendation?= =?UTF-8?q?Response=20dto=EC=97=90=20=EC=A2=8C=ED=91=9C=EC=99=80=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98url=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ClovaRecommendationResponse.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java index 047baa751..59c4e4499 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java @@ -1,5 +1,6 @@ package spring.backend.recommendation.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import spring.backend.activity.domain.value.Keyword; @@ -7,8 +8,20 @@ @Getter @AllArgsConstructor public class ClovaRecommendationResponse { - private Integer order; + @Schema(description = "추천 순서") + private int order; + @Schema(description = "추천 제목") private String title; + @Schema(description = "장소 이름") + private String placeName; + @Schema(description = "장소의 x 좌표") + private String mapx; + @Schema(description = "장소의 y 좌표") + private String mapy; + @Schema(description = "장소의 카카오맵 url (카카오맵만 제공)") + private String placeUrl; + @Schema(description = "추천 부제목") private String content; + @Schema(description = "추천 키워드") private Keyword.Category keywordCategory; } From 35afe38a8d531b41c1031f9c3bd12edcdcb7d534 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 20 Nov 2024 14:50:02 +0900 Subject: [PATCH 331/478] =?UTF-8?q?refactor:=20(#99)=20GetRecommendationsF?= =?UTF-8?q?romClovaService=EC=97=90=EC=84=9C=20Kakao=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 6dc2e6aaf..2a58248aa 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -9,6 +9,7 @@ import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; +import spring.backend.recommendation.infrastructure.map.kakao.dto.response.KakaoMapResponse; import java.util.ArrayList; import java.util.Arrays; @@ -26,6 +27,7 @@ public class GetRecommendationsFromClovaService { private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title :.*"); private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title :"); + private static final Pattern PLACE_NAME_PREFIX_PATTERN = Pattern.compile(".*placeName :"); private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); private static final String LINE_SEPARATOR = "\n"; @@ -38,6 +40,7 @@ public class GetRecommendationsFromClovaService { private static final String SOCIAL = "소셜"; private final RecommendationProvider recommendationProvider; + private final PlaceInfoProvider kakaomapPlaceInfoProvider; public List getRecommendationsFromClova(AIRecommendationRequest clovaRecommendationRequest) { validateLocation(clovaRecommendationRequest); @@ -59,17 +62,16 @@ public List getRecommendationsFromClova(AIRecommend return validRecommendations; } - List filteredValidRecommendations(List clovaResponses) { + private List filteredValidRecommendations(List clovaResponses) { return clovaResponses.stream() .filter(clovaResponse -> clovaResponse.getKeywordCategory() != null && isValidKeywordCategory(clovaResponse.getKeywordCategory())).collect(Collectors.toList()); } - public List fetchRecommendations(AIRecommendationRequest clovaRecommendationRequest) { + private List fetchRecommendations(AIRecommendationRequest clovaRecommendationRequest) { validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); ClovaResponse clovaResponse = recommendationProvider.getRecommendations(clovaRecommendationRequest); validateClovaResponse(clovaResponse); String parsedClovaResponse = clovaResponse.getResult().getMessage().getContent(); - String[] recommendations = parsedClovaResponse.split(LINE_SEPARATOR); List clovaResponses = new ArrayList<>(); @@ -85,6 +87,21 @@ public List fetchRecommendations(AIRecommendationRe i++; } + String placeName = "", placeUrl = "", mapx = "", mapy = ""; + + if (i + 1 < recommendations.length && PLACE_NAME_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { + placeName = PLACE_NAME_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); + KakaoMapResponse placeInfo = kakaomapPlaceInfoProvider.search(placeName); + + if (placeInfo.documents() != null && !placeInfo.documents().isEmpty()) { + mapx = placeInfo.documents().get(0).x(); + mapy = placeInfo.documents().get(0).y(); + placeUrl = placeInfo.documents().get(0).place_url(); + } + + i++; + } + String content = ""; if (i + 1 < recommendations.length && CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { content = CONTENT_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); @@ -97,7 +114,7 @@ public List fetchRecommendations(AIRecommendationRe keywordCategory = convertClovaResponseKeywordToKeywordCategory(keywordText); i++; } - clovaResponses.add(new ClovaRecommendationResponse(order, title, content, keywordCategory)); + clovaResponses.add(new ClovaRecommendationResponse(order, title, placeName, mapx, mapy, placeUrl, content, keywordCategory)); order++; } } From 1388af89b219e4bcfd1cd233b8ff9cc69f92bb3d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 21 Nov 2024 17:13:46 +0900 Subject: [PATCH 332/478] =?UTF-8?q?refactor:=20(#95)=20KakaoPlaceInfoProvi?= =?UTF-8?q?der=EC=9D=98=20url=EC=9D=84=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?UTF-8?q?=ED=8B=B0=EB=A1=9C=20=EA=B4=80=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/KakaoPlaceInfoProvider.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java index 8a9d2c7fd..061c642e9 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/application/KakaoPlaceInfoProvider.java @@ -13,19 +13,23 @@ @Log4j2 public class KakaoPlaceInfoProvider implements PlaceInfoProvider { - @Value("${kakao.rest-api-key}") - private String restApiKey; - - private static final String BASE_URL = "https://dapi.kakao.com"; - private static final String SEARCH_PATH = "/v2/local/search/keyword.json"; + private final String restApiKey; + private final String searchPath; private static final String QUERY = "query"; private static final String AUTHORIZATION = "Authorization"; private static final String KAKAO_AK = "KakaoAK "; private final WebClient webClient; - public KakaoPlaceInfoProvider() { - this.webClient = WebClient.create(BASE_URL); + public KakaoPlaceInfoProvider( + @Value("${kakao.rest-api-key}") String restApiKey, + @Value("${kakao.base-url}") String baseUrl, + @Value("${kakao.search-path}") String searchPath + + ) { + this.restApiKey = restApiKey; + this.searchPath = searchPath; + this.webClient = WebClient.create(baseUrl); } @Override @@ -33,7 +37,7 @@ public KakaoMapResponse search(String query) { try { return webClient.get() .uri(uriBuilder -> uriBuilder - .path(SEARCH_PATH) + .path(searchPath) .queryParam(QUERY, query) .build()) .header(AUTHORIZATION, KAKAO_AK + restApiKey) From 1614961982e1b8f0dae21927a113ad467429e771 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 21 Nov 2024 17:13:58 +0900 Subject: [PATCH 333/478] =?UTF-8?q?refactor:=20(#99)=20KakaoMapResponse=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=A5=BC=20=EC=B9=B4=EB=A9=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EB=B3=80=ED=99=98=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 2 +- .../kakao/dto/response/KakaoMapResponse.java | 35 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 2a58248aa..98e2db8f5 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -96,7 +96,7 @@ private List fetchRecommendations(AIRecommendationR if (placeInfo.documents() != null && !placeInfo.documents().isEmpty()) { mapx = placeInfo.documents().get(0).x(); mapy = placeInfo.documents().get(0).y(); - placeUrl = placeInfo.documents().get(0).place_url(); + placeUrl = placeInfo.documents().get(0).placeUrl(); } i++; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java index fa70edfdc..3d597e31b 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/map/kakao/dto/response/KakaoMapResponse.java @@ -1,5 +1,7 @@ package spring.backend.recommendation.infrastructure.map.kakao.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public record KakaoMapResponse( @@ -7,31 +9,42 @@ public record KakaoMapResponse( Meta meta ) { public record Document( - String address_name, - String category_group_code, - String category_group_name, - String category_name, + @JsonProperty("address_name") + String addressName, + @JsonProperty("category_group_code") + String categoryGroupCode, + @JsonProperty("category_group_name") + String categoryGroupName, + @JsonProperty("category_name") + String categoryName, String distance, String id, String phone, - String place_name, - String place_url, - String road_address_name, + @JsonProperty("place_name") + String placeName, + @JsonProperty("place_url") + String placeUrl, + @JsonProperty("road_address_name") + String roadAddressName, String x, String y ) { } public record Meta( - boolean is_end, - int pageable_count, - int total_count, + @JsonProperty("is_end") + boolean isEnd, + @JsonProperty("pageable_count") + int pageableCount, + @JsonProperty("total_count") + int totalCount, SameName sameName ) { public record SameName( List region, List keyword, - List selected_region + @JsonProperty("selected_region") + List selectedRegion ) { } } From 11df997d98cfc2f9fabb8e5cf870d6bb04563a30 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 21:39:37 +0900 Subject: [PATCH 334/478] =?UTF-8?q?feat:=20(#114)=20activityType=EC=9D=B4?= =?UTF-8?q?=20ONLINE=5FAND=5FOFFLINE=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=203=EA=B0=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 22 +++++--- .../clova/dto/request/ClovaStudioPrompt.java | 50 ++++++++----------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 98e2db8f5..743259d8d 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -31,13 +31,14 @@ public class GetRecommendationsFromClovaService { private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); private static final String LINE_SEPARATOR = "\n"; - private static final String SELF_DEVELOPMENT = "자기개발"; - private static final String HEALTH = "건강"; - private static final String NATURE = "자연"; - private static final String CULTURE_ART = "문화/예술"; - private static final String ENTERTAINMENT = "엔터테인먼트"; - private static final String RELAXATION = "휴식"; - private static final String SOCIAL = "소셜"; + private static final String SELF_DEVELOPMENT = "SELF_DEVELOPMENT"; + private static final String HEALTH = "HEALTH"; + private static final String NATURE = "NATURE"; + private static final String CULTURE_ART = "CULTURE_ART"; + private static final String ENTERTAINMENT = "ENTERTAINMENT"; + private static final String RELAXATION = "RELAXATION"; + private static final String SOCIAL = "SOCIAL"; + private static final int ONLINE_AND_OFFLINE_RECOMMENDATION_COUNT = 3; private final RecommendationProvider recommendationProvider; private final PlaceInfoProvider kakaomapPlaceInfoProvider; @@ -59,7 +60,14 @@ public List getRecommendationsFromClova(AIRecommend throw ClovaErrorCode.INVALID_KEYWORD_IN_RECOMMENDATIONS.toException(); } + if (clovaRecommendationRequest.activityType() == ONLINE_AND_OFFLINE) { + return validRecommendations.stream() + .limit(ONLINE_AND_OFFLINE_RECOMMENDATION_COUNT) + .collect(Collectors.toList()); + } + return validRecommendations; + } private List filteredValidRecommendations(List clovaResponses) { diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index 74828d25a..9cf03619d 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -5,24 +5,20 @@ public class ClovaStudioPrompt { "\n" + "너는 사용자가 입력한 정보를 바탕으로 자투리 시간에 할 수 있는 활동을 추천하는 AI 봇이야. 사용자가 제공하는 정보를 기반으로 적합한 활동을 5가지 추천해줘.\n" + "\n" + - "---\n" + - "\n" + "**입력 정보:**\n" + "\n" + "1. **자투리 시간**: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등).\n" + "2. **활동 타입**:\n" + - " - `OFFLINE`: 오프라인 활동 추천\n" + + " - **`OFFLINE`**: 오프라인 활동 추천\n" + "3. **활동 키워드**: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등).\n" + "4. **위치**: 사용자 위치 정보.\n" + "\n" + - "---\n" + - "\n" + "**추천 기준:**\n" + "\n" + - "1. **활동 타입이 `OFFLINE`일 경우:**\n" + - " - 입력된 활동 키워드와 위치를 고려하여 근처의 특정한 활동 장소를 주소와 함께 추천.\n" + - "\n" + - "---\n" + + "1. **입력된 활동 keyword와 location을 고려하여 근처의 특정한 활동 장소를 주소와 함께 추천.**\n" + + "2. **keyword 선택:**\n" + + " - 추천 활동 키워드는 반드시 무조건 사용자가 입력한 활동 키워드들에 해당하는 활동만 추천.\n" + + " - keyword는 SELF_DEVELOPMENT, ENTERTAINMENT, RELAXATION, CULTURE_ART, HEALTH, NATURE 중 하나만 선택.\n" + "\n" + "**활동 키워드별 정의와 예시:**\n" + "\n" + @@ -45,22 +41,18 @@ public class ClovaStudioPrompt { " - **정의**: 자연과의 접촉을 통해 휴식과 치유를 얻는 활동\n" + " - **예시**: 자연 감상하기, 근처 공원이나 산 둘러보기, 식물원 및 수목원 방문하기 등\n" + "\n" + - "---\n" + - "\n" + "**출력 형식:**\n" + "\n" + - "### 원하는 활동 타입 == OFFLINE:\n" + + "## **원하는 활동 타입 == OFFLINE:**\n" + "\n" + "- title: [활동 제목 또는 추천장소]\n" + "- placeName: [활동 장소 또는 추천장소의 이름]\n" + "- content: [활동 부제목]\n" + "- keyword: [활동 키워드]\n" + "\n" + - "---\n" + - "\n" + "**예시 입력과 출력:**\n" + "\n" + - "### 예시 (활동 타입 == OFFLINE)\n" + + "## **예시 (활동 타입 == OFFLINE)**\n" + "\n" + "**입력:**\n" + "\n" + @@ -76,33 +68,31 @@ public class ClovaStudioPrompt { "content: 독서는 마음의 양식!\n" + "keyword: SELF_DEVELOPMENT\n" + "\n" + - "title: 현대미술 작품을 만날 수 있는 국립현대미술관 방문하기 \n" + + "title: 현대미술 작품을 만날 수 있는 국립현대미술관 방문하기\n" + "placeName: 국립현대미술관\n" + - "content: 예술과 가까워지는 시간! \n" + + "content: 예술과 가까워지는 시간!\n" + "keyword: CULTURE_ART\n" + "\n" + - "title: 그라운드시소 명동에서 지금 핫한 전시 관람하기 \n" + + "title: 그라운드시소 명동에서 지금 핫한 전시 관람하기\n" + "placeName: 그라운드시소 명동\n" + - "content: 도심 속 예술 전시! \n" + + "content: 도심 속 예술 전시!\n" + "keyword: CULTURE_ART\n" + "\n" + - "title: 명동 거리에서 최신 패션 아이템 구경하며 쇼핑하기 \n" + + "title: 명동 거리에서 최신 패션 아이템 구경하며 쇼핑하기\n" + "placeName: 명동거리\n" + "content: 명동에서 아이 쇼핑!\n" + - "keyword: SELF_DEVELOPMENT\n" + + "keyword: ENTERTAINMENT\n" + "\n" + - "title: 팝마트 명동 프리미엄 테마샵에서 피규어와 장난감 구경하기 \n" + + "title: 팝마트 명동 프리미엄 테마샵에서 피규어와 장난감 구경하기\n" + "placeName: 팝마트 명동 프리미엄 테마샵\n" + - "content: 동심으로 돌아가는 시간! \n" + - "keyword: SELF_DEVELOPMENT\n" + - "\n" + - "---\n" + + "content: 동심으로 돌아가는 시간!\n" + + "keyword: ENTERTAINMENT\n" + "\n" + "**유의사항:**\n" + "\n" + - "1. `keyword`는 반드시 한 개만 반환합니다.\n" + - "2. `placeName` 은 반드시 활동 장소 또는 추천 장소의 이름(명사)의 형태로 제공합니다.\n" + + "1. **`keyword`**는 반드시 한 개만 반환하며, 사용자가 입력한 활동 키워드 중에서만 선택합니다.\n" + + "2. **`placeName`** 은 반드시 활동 장소 또는 추천 장소의 이름(명사)의 형태로 제공합니다.\n" + "3. 추천은 5개여야 합니다.\n" + "4. 활동 소요 시간을 기준으로 추천 활동을 설계. spareTime으로 주어진 시간을 알차게 활용할 수 있는 활동만 제공합니다.\n" + - "5. `title`, `placeName`, `content`, `keyword`의 구조를 유지하면서도 내용이 중복되지 않도록 세부 사항을 차별화합니다."; -} + "5. **`title`**, **`placeName`**, **`content`**, **`keyword`**의 구조를 유지하면서도 내용이 중복되지 않도록 세부 사항을 차별화합니다"; + } From 389a4ffafe67bbd5079050cc79dddf96c93f84e5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 23:45:25 +0900 Subject: [PATCH 335/478] =?UTF-8?q?refactor:=20(#114)=20String=20=EA=B0=92?= =?UTF-8?q?=EA=B3=BC=20Keyword.Category=EB=A5=BC=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=8A=A4=EC=9C=84=EC=B9=98=EB=AC=B8?= =?UTF-8?q?=EC=9D=84=20Enum=20=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 27 +++++++------------ .../clova/dto/request/ClovaStudioPrompt.java | 2 +- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 743259d8d..6a6655b14 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -31,13 +31,6 @@ public class GetRecommendationsFromClovaService { private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); private static final String LINE_SEPARATOR = "\n"; - private static final String SELF_DEVELOPMENT = "SELF_DEVELOPMENT"; - private static final String HEALTH = "HEALTH"; - private static final String NATURE = "NATURE"; - private static final String CULTURE_ART = "CULTURE_ART"; - private static final String ENTERTAINMENT = "ENTERTAINMENT"; - private static final String RELAXATION = "RELAXATION"; - private static final String SOCIAL = "SOCIAL"; private static final int ONLINE_AND_OFFLINE_RECOMMENDATION_COUNT = 3; private final RecommendationProvider recommendationProvider; @@ -119,7 +112,9 @@ private List fetchRecommendations(AIRecommendationR Keyword.Category keywordCategory = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); + log.info("keywordText: {}", keywordText); keywordCategory = convertClovaResponseKeywordToKeywordCategory(keywordText); + log.info("keywordCategory: {}", keywordCategory); i++; } clovaResponses.add(new ClovaRecommendationResponse(order, title, placeName, mapx, mapy, placeUrl, content, keywordCategory)); @@ -172,15 +167,13 @@ private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest c } private Keyword.Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { - return switch (keywordText) { - case SELF_DEVELOPMENT -> Keyword.Category.SELF_DEVELOPMENT; - case HEALTH -> Keyword.Category.HEALTH; - case NATURE -> Keyword.Category.NATURE; - case CULTURE_ART -> Keyword.Category.CULTURE_ART; - case ENTERTAINMENT -> Keyword.Category.ENTERTAINMENT; - case RELAXATION -> Keyword.Category.RELAXATION; - case SOCIAL -> Keyword.Category.SOCIAL; - default -> null; - }; + try { + return Keyword.Category.valueOf(keywordText); + } catch (IllegalArgumentException e) { + return Arrays.stream(Keyword.Category.values()) + .filter(category -> category.getDescription().equals(keywordText)) + .findFirst() + .orElse(null); + } } } diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index 9cf03619d..81ff951a6 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -18,7 +18,7 @@ public class ClovaStudioPrompt { "1. **입력된 활동 keyword와 location을 고려하여 근처의 특정한 활동 장소를 주소와 함께 추천.**\n" + "2. **keyword 선택:**\n" + " - 추천 활동 키워드는 반드시 무조건 사용자가 입력한 활동 키워드들에 해당하는 활동만 추천.\n" + - " - keyword는 SELF_DEVELOPMENT, ENTERTAINMENT, RELAXATION, CULTURE_ART, HEALTH, NATURE 중 하나만 선택.\n" + + " - keyword는 SELF_DEVELOPMENT, ENTERTAINMENT, RELAXATION, CULTURE_ART, HEALTH, NATURE 중 하나로 반환.\n" + "\n" + "**활동 키워드별 정의와 예시:**\n" + "\n" + From af772732c3afda6bd4211a5145e7b10eb69a5da9 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 22 Nov 2024 04:43:03 +0900 Subject: [PATCH 336/478] =?UTF-8?q?feat:=20(#109)=20Spring=20Batch=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 57aeb9774..d2a606fc7 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,10 @@ dependencies { // Rabbit MQ implementation 'org.springframework.boot:spring-boot-starter-amqp' testImplementation 'org.springframework.amqp:spring-rabbit-test' + + // Spring Batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + testImplementation 'org.springframework.batch:spring-batch-test' } tasks.named('test') { From b97fb4f1ec5ef092cf64f300c248cd6236ea5a59 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 22 Nov 2024 06:23:46 +0900 Subject: [PATCH 337/478] =?UTF-8?q?fix:=20(#109)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=8B=A0=EC=9E=90=EB=A5=BC=20=EC=97=AC?= =?UTF-8?q?=EB=9F=AC=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/util/email/EmailUtil.java | 13 +++++++------ .../util/email/dto/request/SendEmailRequest.java | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index 9cdf998f1..3fe784cd7 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -9,6 +9,7 @@ import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.core.util.email.exception.MailErrorCode; +import java.util.Arrays; import java.util.regex.Pattern; @Component @@ -29,13 +30,13 @@ public void send(SendEmailRequest sendEmailRequest) { try { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(sender); - message.setTo(sendEmailRequest.receiver()); + message.setTo(sendEmailRequest.receivers()); message.setSubject(sendEmailRequest.title()); message.setText(sendEmailRequest.content()); mailSender.send(message); } catch (MailParseException e) { log.error("[EmailUtil] Failed to parse email for recipient: {}, subject: {}. Error: {}", - sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); + Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.FAILED_TO_PARSE_MAIL.toException(); } catch (MailAuthenticationException e) { log.error("[EmailUtil] Authentication failed for email sender: {}. Error: {}", @@ -43,11 +44,11 @@ public void send(SendEmailRequest sendEmailRequest) { throw MailErrorCode.AUTHENTICATION_FAILED.toException(); } catch (MailSendException e) { log.error("[EmailUtil] Error occurred while sending email to recipient: {}, subject: {}. Error: {}", - sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); + Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.ERROR_OCCURRED_SENDING_MAIL.toException(); } catch (MailException e) { log.error("[EmailUtil] General mail error for recipient: {}, subject: {}. Error: {}", - sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); + Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.GENERAL_MAIL_ERROR.toException(); } } @@ -59,8 +60,8 @@ private void validateEmailRequest(SendEmailRequest request) { } private void validateEmailAddress(SendEmailRequest request) { - if (request.receiver() == null || !EMAIL_PATTERN.matcher(request.receiver()).matches()) { - log.error("[EmailUtil] Invalid email address format: {}", request.receiver()); + if (request.receivers() == null || Arrays.stream(request.receivers()).anyMatch(email -> !EMAIL_PATTERN.matcher(email).matches())) { + log.error("[EmailUtil] Invalid email address format: {}", Arrays.toString(request.receivers())); throw MailErrorCode.INVALID_MAIL_ADDRESS.toException(); } } diff --git a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java index 691a07b75..16b0d558c 100644 --- a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java +++ b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java @@ -1,7 +1,10 @@ package spring.backend.core.util.email.dto.request; +import lombok.Builder; + +@Builder public record SendEmailRequest( - String receiver, + String[] receivers, String title, String content ) { From d96d3686a74f72f5a73c25839e2813ce3552a897 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 22 Nov 2024 06:25:19 +0900 Subject: [PATCH 338/478] =?UTF-8?q?feat:=20(#109)=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EB=82=B4=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=EC=9D=84=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=B3=84=201?= =?UTF-8?q?=EA=B0=9C=EC=94=A9=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20JPQ?= =?UTF-8?q?L=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/repository/QuickStartJpaRepository.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java index 548dafd6c..955132c82 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java @@ -1,7 +1,24 @@ package spring.backend.activity.infrastructure.persistence.jpa.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import java.time.LocalTime; +import java.util.List; + public interface QuickStartJpaRepository extends JpaRepository { + + @Query(""" + select q + from QuickStartJpaEntity q + where q.startTime between :lowerBound and :upperBound + and q.id in ( + select min(q2.id) + from QuickStartJpaEntity q2 + where q2.memberId = q.memberId + group by q2.memberId + ) + """) + List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound); } From 999db50c1e44a58aa36b3c37b33e451bda12ac89 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 22 Nov 2024 06:27:33 +0900 Subject: [PATCH 339/478] =?UTF-8?q?feat:=20(#109)=20Batch=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=EC=84=9C=2015=EB=B6=84=20=EC=A3=BC?= =?UTF-8?q?=EA=B8=B0=EB=A1=9C=20=EB=B9=A0=EB=A5=B8=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=A9=94=EC=9D=BC=EC=9D=84=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/BackendApplication.java | 2 + .../batch/job/SendQuickStartEmailsJob.java | 117 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java diff --git a/src/main/java/spring/backend/BackendApplication.java b/src/main/java/spring/backend/BackendApplication.java index 5fe6517dd..698b1e56f 100644 --- a/src/main/java/spring/backend/BackendApplication.java +++ b/src/main/java/spring/backend/BackendApplication.java @@ -3,7 +3,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @EnableJpaAuditing @SpringBootApplication public class BackendApplication { diff --git a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java new file mode 100644 index 000000000..2090c6948 --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java @@ -0,0 +1,117 @@ +package spring.backend.activity.infrastructure.batch.job; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; +import spring.backend.core.util.email.EmailUtil; +import spring.backend.core.util.email.dto.request.SendEmailRequest; +import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; +import spring.backend.member.infrastructure.persistence.jpa.repository.MemberJpaRepository; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +@Configuration +@RequiredArgsConstructor +@Log4j2 +public class SendQuickStartEmailsJob { + + private final JobRepository jobRepository; + + private final JobLauncher jobLauncher; + + private final PlatformTransactionManager platformTransactionManager; + + private final QuickStartJpaRepository quickStartJpaRepository; + + private final MemberJpaRepository memberJpaRepository; + + private final EmailUtil emailUtil; + +// @Scheduled(cron = "0 0/15 * * * ?") + public void sendQuickStartEmailsJobScheduler() { + try { + JobParameters jobParameters = new JobParametersBuilder() + .addDate("time", new Date()) + .toJobParameters(); + jobLauncher.run(sendQuickStartEmailsJob(), jobParameters); + } catch (Exception e) { + log.error("[SendQuickStartEmailsJob] Scheduler encountered an error at {}", LocalDateTime.now(), e); + } + } + + public Job sendQuickStartEmailsJob() { + final String JOB_NAME = "sendQuickStartEmailsJob"; + return new JobBuilder(JOB_NAME, jobRepository) + .start(sendQuickStartEmailsStep()) + .build(); + } + + public Step sendQuickStartEmailsStep() { + final String STEP_NAME = "sendQuickStartEmailsStep"; + return new StepBuilder(STEP_NAME, jobRepository) + .tasklet(sendQuickStartEmailsTasklet(), platformTransactionManager) + .build(); + } + + public Tasklet sendQuickStartEmailsTasklet() { + return (contribution, chunkContext) -> { + List receivers = new ArrayList<>(); + try { + final int TIME_INTERVAL_MINUTES = 15; + LocalTime now = LocalTime.now(); + LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); + LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + + log.info("[SendQuickStartEmailsJob] Searching for QuickStarts between {} and {}", lowerBound, upperBound); + + List quickStarts = quickStartJpaRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); + + quickStarts.stream() + .map(QuickStartJpaEntity::getMemberId) + .map(memberJpaRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .map(MemberJpaEntity::getEmail) + .forEach(receivers::add); + + if (!receivers.isEmpty()) { + SendEmailRequest request = SendEmailRequest.builder() + .title("Test Title") + .content("Test Content") + .receivers(receivers.toArray(new String[0])) + .build(); + + try { + emailUtil.send(request); + log.info("[SendQuickStartEmailsJob] Successfully sent email to {} receivers", receivers.size()); + } catch (Exception e) { + log.error("[SendQuickStartEmailsJob] Failed to send email to {} receivers", receivers.size(), e); + } + } else { + log.warn("[SendQuickStartEmailsJob] No valid receivers found in the time range: {} - {}", lowerBound, upperBound); + } + } catch (Exception e) { + log.error("[SendQuickStartEmailsJob] Error during tasklet execution", e); + } + return RepeatStatus.FINISHED; + }; + } +} From b1927b433424d44f4e651236e509c249658cbecd Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 22 Nov 2024 07:23:52 +0900 Subject: [PATCH 340/478] =?UTF-8?q?feat:=20(#109)=20Scheduler=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=EC=84=9C=2015=EB=B6=84=20=EC=A3=BC?= =?UTF-8?q?=EA=B8=B0=EB=A1=9C=20=EB=B9=A0=EB=A5=B8=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=A9=94=EC=9D=BC=EC=9D=84=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SendQuickStartEmailsScheduler.java | 68 +++++++++++++++++++ .../repository/QuickStartRepository.java | 4 ++ .../jpa/adapter/QuickStartRepositoryImpl.java | 16 +++++ 3 files changed, 88 insertions(+) create mode 100644 src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java new file mode 100644 index 000000000..46fcb55a6 --- /dev/null +++ b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java @@ -0,0 +1,68 @@ +package spring.backend.activity.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.core.util.email.EmailUtil; +import spring.backend.core.util.email.dto.request.SendEmailRequest; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class SendQuickStartEmailsScheduler { + + private final QuickStartRepository quickStartRepository; + + private final MemberRepository memberRepository; + + private final EmailUtil emailUtil; + + @Scheduled(cron = "0 0/15 * * * ?") + public void sendQuickStartEmails() { + List receivers = new ArrayList<>(); + + final int TIME_INTERVAL_MINUTES = 15; + LocalTime now = LocalTime.now(); + LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); + LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + + log.info("[SendQuickStartEmailsScheduler] Searching for QuickStarts between {} and {}", lowerBound, upperBound); + + List quickStarts = quickStartRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); + + quickStarts.stream() + .map(QuickStart::getMemberId) + .map(memberRepository::findById) + .filter(Objects::nonNull) + .map(Member::getEmail) + .filter(Objects::nonNull) + .forEach(receivers::add); + + if (!receivers.isEmpty()) { + SendEmailRequest request = SendEmailRequest.builder() + .title("Test Title") + .content("Test Content") + .receivers(receivers.toArray(new String[0])) + .build(); + + try { + emailUtil.send(request); + log.info("[SendQuickStartEmailsScheduler] Successfully sent email to {} receivers", receivers.size()); + } catch (Exception e) { + log.error("[SendQuickStartEmailsScheduler] Failed to send email to {} receivers", receivers.size(), e); + } + } else { + log.warn("[SendQuickStartEmailsScheduler] No valid receivers found in the time range: {} - {}", lowerBound, upperBound); + } + } +} diff --git a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java index 5e483a366..377f2996b 100644 --- a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java +++ b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java @@ -2,8 +2,12 @@ import spring.backend.activity.domain.entity.QuickStart; +import java.time.LocalTime; +import java.util.List; + public interface QuickStartRepository { QuickStart findById(Long id); QuickStart save(QuickStart quickStart); + List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound); } diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java index 1545e6f62..f7dd091d7 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java @@ -10,6 +10,12 @@ import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + @Repository @RequiredArgsConstructor @Log4j2 @@ -38,4 +44,14 @@ public QuickStart save(QuickStart quickStart) { throw QuickStartErrorCode.QUICK_START_SAVE_FAILED.toException(); } } + + @Override + public List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound) { + List quickStartJpaEntities = quickStartJpaRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); + return Optional.ofNullable(quickStartJpaEntities) + .orElse(Collections.emptyList()) + .stream() + .map(quickStartMapper::toDomainEntity) + .collect(Collectors.toList()); + } } From 5068d67be4f340ef81e87632905b890764eda12e Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 02:33:45 +0900 Subject: [PATCH 341/478] =?UTF-8?q?fix:=20(#120)=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EC=97=AC=EB=9F=AC=20=EB=AA=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20Sen?= =?UTF-8?q?dEmailRequest=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=98=EB=A9=B4?= =?UTF-8?q?=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=8F=84=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/core/util/EmailUtilTest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/test/java/spring/backend/core/util/EmailUtilTest.java b/src/test/java/spring/backend/core/util/EmailUtilTest.java index 4ec23197e..b800f3350 100644 --- a/src/test/java/spring/backend/core/util/EmailUtilTest.java +++ b/src/test/java/spring/backend/core/util/EmailUtilTest.java @@ -4,10 +4,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.javamail.JavaMailSender; import spring.backend.core.exception.DomainException; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; @@ -27,7 +24,7 @@ public class EmailUtilTest { @Test void throwExceptionWhenToInRequestIsInvalid() { // GIVEN - sendEmailRequest = new SendEmailRequest("test", "Test Subject", "Test Content"); + sendEmailRequest = new SendEmailRequest(new String[]{"test", "test2"}, "Test Subject", "Test Content"); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "올바르지 않은 이메일 주소입니다."); @@ -51,7 +48,7 @@ void throwExceptionWhenToInRequestIsNull() { @Test void throwExceptionWhenSubjectInRequestIsNull() { // GIVEN - sendEmailRequest = new SendEmailRequest("test@naver.com", "", "Test Content"); + sendEmailRequest = new SendEmailRequest(new String[]{"test@naver.com", "test2@naver.com"}, "", "Test Content"); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 제목이 없습니다."); @@ -63,7 +60,7 @@ void throwExceptionWhenSubjectInRequestIsNull() { @Test void throwExceptionWhenTextInRequestIsNull() { // GIVEN - sendEmailRequest = new SendEmailRequest("test@naver.com", "Test Subject", ""); + sendEmailRequest = new SendEmailRequest(new String[]{"test@naver.com", "test2@naver.com"}, "Test Subject", ""); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 내용이 없습니다."); From d96298eb727b98f28997adac71b82f9a2ba043e3 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 02:15:05 +0900 Subject: [PATCH 342/478] =?UTF-8?q?feat:=20(#118)=20=EC=98=A8=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8,=20=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20API=EB=A5=BC=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/domain/value/Type.java | 8 +++ .../dto/response/RecommendationResponse.java | 18 +++++++ .../GetRecommendationsController.java | 49 +++++++++++++++++++ ...GetRecommendationsFromClovaController.java | 34 ------------- ...etRecommendationsFromOpenAIController.java | 32 ------------ .../GetRecommendationsFromOpenAISwagger.java | 27 ---------- ...er.java => GetRecommendationsSwagger.java} | 11 ++--- 7 files changed, 80 insertions(+), 99 deletions(-) create mode 100644 src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java create mode 100644 src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java delete mode 100644 src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java delete mode 100644 src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java delete mode 100644 src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java rename src/main/java/spring/backend/recommendation/presentation/swagger/{GetRecommendationsFromClovaSwagger.java => GetRecommendationsSwagger.java} (66%) diff --git a/src/main/java/spring/backend/activity/domain/value/Type.java b/src/main/java/spring/backend/activity/domain/value/Type.java index 41b84cb1e..71d742a37 100644 --- a/src/main/java/spring/backend/activity/domain/value/Type.java +++ b/src/main/java/spring/backend/activity/domain/value/Type.java @@ -11,4 +11,12 @@ public enum Type { ONLINE_AND_OFFLINE("온라인과 오프라인 모두"); private final String description; + + public boolean includesOffline() { + return this == OFFLINE || this == ONLINE_AND_OFFLINE; + } + + public boolean includesOnline() { + return this == ONLINE || this == ONLINE_AND_OFFLINE; + } } diff --git a/src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java new file mode 100644 index 000000000..fe3a7b204 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java @@ -0,0 +1,18 @@ +package spring.backend.recommendation.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record RecommendationResponse( + + @Schema(description = "[OFFLINE, ONLINE_AND_OFFLINE] 오프라인 추천 응답") + List offlineRecommendations, + + @Schema(description = "[ONLINE, ONLINE_AND_OFFLINE] 온라인 추천 응답") + List onlineRecommendations +) { + public static RecommendationResponse of(List offlineRecommendations, List onlineRecommendations) { + return new RecommendationResponse(offlineRecommendations, onlineRecommendations); + } +} diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java new file mode 100644 index 000000000..9964e9111 --- /dev/null +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java @@ -0,0 +1,49 @@ +package spring.backend.recommendation.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.recommendation.application.GetRecommendationsFromClovaService; +import spring.backend.recommendation.application.GetRecommendationsFromOpenAIService; +import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.dto.response.RecommendationResponse; +import spring.backend.recommendation.presentation.swagger.GetRecommendationsSwagger; + +import java.util.ArrayList; +import java.util.List; + + +@RestController +@RequiredArgsConstructor +public class GetRecommendationsController implements GetRecommendationsSwagger { + + private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; + + private final GetRecommendationsFromOpenAIService getRecommendationsFromOpenAIService; + + @Authorization + @PostMapping("/v1/recommendations") + public ResponseEntity> getRecommendations(@AuthorizedMember Member member, @Valid @RequestBody AIRecommendationRequest request) { + List offlineRecommendations = new ArrayList<>(); + List onlineRecommendations = new ArrayList<>(); + + if (request.activityType().includesOffline()) { + offlineRecommendations = getRecommendationsFromClovaService.getRecommendationsFromClova(request); + } + if (request.activityType().includesOnline()) { + onlineRecommendations = getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request); + } + + RecommendationResponse recommendationResponse = RecommendationResponse.of(offlineRecommendations, onlineRecommendations); + return ResponseEntity.ok(new RestResponse<>(recommendationResponse)); + } +} diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java deleted file mode 100644 index c75bb2972..000000000 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromClovaController.java +++ /dev/null @@ -1,34 +0,0 @@ -package spring.backend.recommendation.presentation; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import spring.backend.core.configuration.argumentresolver.AuthorizedMember; -import spring.backend.core.configuration.interceptor.Authorization; -import spring.backend.core.presentation.RestResponse; -import spring.backend.member.domain.entity.Member; -import spring.backend.recommendation.application.GetRecommendationsFromClovaService; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; -import spring.backend.recommendation.presentation.swagger.GetRecommendationsFromClovaSwagger; - -import java.util.List; - - -@RestController -@RequiredArgsConstructor -@RequestMapping("/v1/recommendations") -public class GetRecommendationsFromClovaController implements GetRecommendationsFromClovaSwagger { - private final GetRecommendationsFromClovaService getRecommendationsFromClovaService; - - @Authorization - @PostMapping - public ResponseEntity>> requestRecommendations(@AuthorizedMember Member member, @Valid @RequestBody AIRecommendationRequest aiRecommendationRequest) { - List response = getRecommendationsFromClovaService.getRecommendationsFromClova(aiRecommendationRequest); - return ResponseEntity.ok(new RestResponse<>(response)); - } -} diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java deleted file mode 100644 index 2bf3c0729..000000000 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsFromOpenAIController.java +++ /dev/null @@ -1,32 +0,0 @@ -package spring.backend.recommendation.presentation; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; -import spring.backend.core.configuration.argumentresolver.AuthorizedMember; -import spring.backend.core.configuration.interceptor.Authorization; -import spring.backend.core.presentation.RestResponse; -import spring.backend.member.domain.entity.Member; -import spring.backend.recommendation.application.GetRecommendationsFromOpenAIService; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; -import spring.backend.recommendation.presentation.swagger.GetRecommendationsFromOpenAISwagger; - -import java.util.List; - -@RestController -@RequiredArgsConstructor -public class GetRecommendationsFromOpenAIController implements GetRecommendationsFromOpenAISwagger { - - private final GetRecommendationsFromOpenAIService getRecommendationsFromOpenAIService; - - @Authorization - @PostMapping("/v1/recommendations/open-ai") - public ResponseEntity>> GetRecommendationsFromOpenAI(@AuthorizedMember Member member, @Valid @RequestBody AIRecommendationRequest request) { - List recommendationsFromOpenAI = getRecommendationsFromOpenAIService.getRecommendationsFromOpenAI(request); - return ResponseEntity.ok(new RestResponse<>(recommendationsFromOpenAI)); - } -} diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java deleted file mode 100644 index 809686605..000000000 --- a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromOpenAISwagger.java +++ /dev/null @@ -1,27 +0,0 @@ -package spring.backend.recommendation.presentation.swagger; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; -import spring.backend.core.configuration.swagger.ApiErrorCode; -import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.core.presentation.RestResponse; -import spring.backend.member.domain.entity.Member; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; -import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; - -import java.util.List; - -@Tag(name = "Recommendation", description = "추천") -public interface GetRecommendationsFromOpenAISwagger { - - @Operation( - summary = "[ONLINE, ONLINE_AND_OFFLINE] 사용자 추천 요청 API", - description = "[ONLINE, ONLINE_AND_OFFLINE] 사용자가 활동 추천을 요청합니다.", - operationId = "/v1/recommendations/open-ai" - ) - @ApiErrorCode({GlobalErrorCode.class, OpenAIErrorCode.class}) - ResponseEntity>> GetRecommendationsFromOpenAI(@Parameter(hidden = true) Member member, AIRecommendationRequest request); -} diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java similarity index 66% rename from src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java rename to src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java index a49a2c30c..88a58a9b5 100644 --- a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsFromClovaSwagger.java +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java @@ -9,19 +9,18 @@ import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.dto.response.RecommendationResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; - -import java.util.List; +import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; @Tag(name = "Recommendation", description = "추천") -public interface GetRecommendationsFromClovaSwagger { +public interface GetRecommendationsSwagger { @Operation( summary = "사용자 추천 요청 API", description = "사용자가 활동 추천을 요청합니다.", operationId = "/v1/recommendations" ) - @ApiErrorCode({GlobalErrorCode.class, ClovaErrorCode.class}) - ResponseEntity>> requestRecommendations(@Parameter(hidden = true) Member member, AIRecommendationRequest aiRecommendationRequest); + @ApiErrorCode({GlobalErrorCode.class, ClovaErrorCode.class, OpenAIErrorCode.class}) + ResponseEntity> getRecommendations(@Parameter(hidden = true) Member member, AIRecommendationRequest request); } From 92256afe586318fa5e8fcb5b70229b25cfb821af Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 15:35:50 +0900 Subject: [PATCH 343/478] =?UTF-8?q?fix:=20(#127)=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=EC=9D=BC=20=EB=95=8C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20DTO=EC=97=90=20=EC=A4=91=EB=B3=B5=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/ValidateNicknameService.java | 10 +++++----- .../presentation/swagger/ValidateNicknameSwagger.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/member/application/ValidateNicknameService.java b/src/main/java/spring/backend/member/application/ValidateNicknameService.java index d7e0fad32..13a922726 100644 --- a/src/main/java/spring/backend/member/application/ValidateNicknameService.java +++ b/src/main/java/spring/backend/member/application/ValidateNicknameService.java @@ -5,7 +5,6 @@ import org.springframework.stereotype.Service; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.domain.value.Role; -import spring.backend.member.exception.MemberErrorCode; @Service @RequiredArgsConstructor @@ -17,18 +16,19 @@ public class ValidateNicknameService { public boolean validateNickname(String nickname) { if (nickname == null || nickname.isBlank()) { log.error("[ValidateNicknameService] Nickname is empty"); - throw MemberErrorCode.NOT_EXIST_NICKNAME.toException(); + return false; } if (nickname.length() > 6) { log.error("[ValidateNicknameService] Nickname is smaller than 6 characters"); - throw MemberErrorCode.INVALID_NICKNAME_LENGTH.toException(); + return false; } if (!nickname.matches("^[a-zA-Z0-9가-힣]+$")) { log.error("[ValidateNicknameService] Nickname is invalid"); - throw MemberErrorCode.INVALID_NICKNAME_FORMAT.toException(); + return false; } if (memberRepository.existsByNicknameAndRole(nickname, Role.MEMBER)) { - throw MemberErrorCode.ALREADY_REGISTERED_NICKNAME.toException(); + log.error("[ValidateNicknameService] Nickname is already in use"); + return false; } return true; } diff --git a/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java index 9ce73b0c4..36af74fad 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/ValidateNicknameSwagger.java @@ -14,7 +14,7 @@ public interface ValidateNicknameSwagger { @Operation( summary = "닉네임 중복 검증 API", - description = "닉네임이 조건에 충족하지 않거나 중복일 경우, 에러를 발생합니다.", + description = "닉네임이 조건에 충족하지 않거나 중복일 경우 false를 반환합니다.", operationId = "/v1/members/check-nickname" ) @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) From c09b209ced4ec00a7b7ae66a2b781e0ae8125711 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 15:48:17 +0900 Subject: [PATCH 344/478] =?UTF-8?q?fix:=20(#129)=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84=20=EA=B2=80=EC=A6=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=98=ED=99=98=20=EA=B0=92=EC=9D=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=98=EB=A9=B4=EC=84=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ValidateNicknameServiceTest.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java b/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java index 74887e6ce..ca690a1b9 100644 --- a/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java +++ b/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java @@ -6,13 +6,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import spring.backend.core.exception.DomainException; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.domain.value.Role; -import spring.backend.member.exception.MemberErrorCode; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,11 +32,8 @@ void throwExceptionWhenNicknameIsBlank() { // Given String nickname = " "; - // When - DomainException blankException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); - - // Then - assertEquals(MemberErrorCode.NOT_EXIST_NICKNAME.name(), blankException.getCode()); + // When & Then + assertFalse(validateNicknameService.validateNickname(nickname)); } @Test @@ -48,11 +42,8 @@ void throwExceptionWhenNicknameLengthIsInvalid() { // Given String nickname = "1234567"; - // When - DomainException longLengthException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); - - // Then - assertEquals(MemberErrorCode.INVALID_NICKNAME_LENGTH.name(), longLengthException.getCode()); + // When & Then + assertFalse(validateNicknameService.validateNickname(nickname)); } @Test @@ -61,11 +52,8 @@ void throwExceptionWhenNicknameFormatIsInvalid() { // Given String nickname = "조각ㅈㄱ"; - // When - DomainException formatException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); - - // Then - assertEquals(MemberErrorCode.INVALID_NICKNAME_FORMAT.name(), formatException.getCode()); + // When & Then + assertFalse(validateNicknameService.validateNickname(nickname)); } @Test @@ -75,13 +63,10 @@ void throwExceptionWhenNicknameIsAlreadyRegistered() { String nickname = "등록된이름"; when(memberRepository.existsByNicknameAndRole(nickname, Role.MEMBER)).thenReturn(true); - // When - DomainException alreadyRegisteredException = assertThrows(DomainException.class, () -> validateNicknameService.validateNickname(nickname)); - - // Then - assertEquals(MemberErrorCode.ALREADY_REGISTERED_NICKNAME.name(), alreadyRegisteredException.getCode()); + // When & Then + assertFalse(validateNicknameService.validateNickname(nickname)); // Mock 객체 정상 동작 확인 verify(memberRepository).existsByNicknameAndRole(nickname, Role.MEMBER); } -} \ No newline at end of file +} From e96778f9d61145eea2b3068afa90e876d4cd1023 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 14:37:21 +0900 Subject: [PATCH 345/478] =?UTF-8?q?refactor:=20(#123)=20ClovaStudioPrompt?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clova/dto/request/ClovaStudioPrompt.java | 158 +++++++----------- 1 file changed, 63 insertions(+), 95 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index 81ff951a6..f59a9fe52 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -1,98 +1,66 @@ package spring.backend.recommendation.infrastructure.clova.dto.request; public class ClovaStudioPrompt { - public static final String DEFAULT_SYSTEM_PROMPT = "**역할:**\n" + - "\n" + - "너는 사용자가 입력한 정보를 바탕으로 자투리 시간에 할 수 있는 활동을 추천하는 AI 봇이야. 사용자가 제공하는 정보를 기반으로 적합한 활동을 5가지 추천해줘.\n" + - "\n" + - "**입력 정보:**\n" + - "\n" + - "1. **자투리 시간**: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등).\n" + - "2. **활동 타입**:\n" + - " - **`OFFLINE`**: 오프라인 활동 추천\n" + - "3. **활동 키워드**: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등).\n" + - "4. **위치**: 사용자 위치 정보.\n" + - "\n" + - "**추천 기준:**\n" + - "\n" + - "1. **입력된 활동 keyword와 location을 고려하여 근처의 특정한 활동 장소를 주소와 함께 추천.**\n" + - "2. **keyword 선택:**\n" + - " - 추천 활동 키워드는 반드시 무조건 사용자가 입력한 활동 키워드들에 해당하는 활동만 추천.\n" + - " - keyword는 SELF_DEVELOPMENT, ENTERTAINMENT, RELAXATION, CULTURE_ART, HEALTH, NATURE 중 하나로 반환.\n" + - "\n" + - "**활동 키워드별 정의와 예시:**\n" + - "\n" + - "1. **SELF_DEVELOPMENT**\n" + - " - **정의**: 시사상식, 지식, 교양과 관련된 활동으로, 개인의 성장과 발전을 위한 것\n" + - " - **예시:** 서점 및 도서관 방문하여 책 읽기, 박물관 방문하기 등\n" + - "2. **ENTERTAINMENT**\n" + - " - **정의**: 즐거움과 오락을 목적으로 한 활동, 순간의 재미와 유희를 위한 것\n" + - " - **예시**: 쇼핑하기, 노래방 가기, 영화보기, 볼링치기 등\n" + - "3. **RELAXATION**\n" + - " - **정의**: 신체적, 정신적 피로 회복과 재충전을 위한 정적인 활동\n" + - " - **예시**: 찻집 가기, 공원 걷기, 자연 감상하기 등\n" + - "4. **CULTURE_ART**\n" + - " - **정의**: 예술적, 문화적 경험과 감상을 통해 영감과 인사이트를 얻는 활동\n" + - " - **예시**: 전시회 관람하기, 갤러리 카페 방문하기, 미술관 방문하기 등\n" + - "5. **HEALTH**\n" + - " - **정의**: 신체적, 정신적 건강을 개선하고 유지하기 위한 활동, 스포츠 중심\n" + - " - **예시**: 공원에서 러닝하기, 스트레칭하기, 각종 스포츠 활동하기 등\n" + - "6. **NATURE**\n" + - " - **정의**: 자연과의 접촉을 통해 휴식과 치유를 얻는 활동\n" + - " - **예시**: 자연 감상하기, 근처 공원이나 산 둘러보기, 식물원 및 수목원 방문하기 등\n" + - "\n" + - "**출력 형식:**\n" + - "\n" + - "## **원하는 활동 타입 == OFFLINE:**\n" + - "\n" + - "- title: [활동 제목 또는 추천장소]\n" + - "- placeName: [활동 장소 또는 추천장소의 이름]\n" + - "- content: [활동 부제목]\n" + - "- keyword: [활동 키워드]\n" + - "\n" + - "**예시 입력과 출력:**\n" + - "\n" + - "## **예시 (활동 타입 == OFFLINE)**\n" + - "\n" + - "**입력:**\n" + - "\n" + - "- 자투리 시간: 30분\n" + - "- 선호 활동 타입: OFFLINE\n" + - "- 위치: 서울특별시 중구 명동\n" + - "- 활동 키워드: SELF_DEVELOPMENT, CULTURE_ART, ENTERTAINMENT\n" + - "\n" + - "**출력:**\n" + - "\n" + - "title: 서울도서관에서 인사이트 가득한 책 읽기\n" + - "placeName: 서울도서관\n" + - "content: 독서는 마음의 양식!\n" + - "keyword: SELF_DEVELOPMENT\n" + - "\n" + - "title: 현대미술 작품을 만날 수 있는 국립현대미술관 방문하기\n" + - "placeName: 국립현대미술관\n" + - "content: 예술과 가까워지는 시간!\n" + - "keyword: CULTURE_ART\n" + - "\n" + - "title: 그라운드시소 명동에서 지금 핫한 전시 관람하기\n" + - "placeName: 그라운드시소 명동\n" + - "content: 도심 속 예술 전시!\n" + - "keyword: CULTURE_ART\n" + - "\n" + - "title: 명동 거리에서 최신 패션 아이템 구경하며 쇼핑하기\n" + - "placeName: 명동거리\n" + - "content: 명동에서 아이 쇼핑!\n" + - "keyword: ENTERTAINMENT\n" + - "\n" + - "title: 팝마트 명동 프리미엄 테마샵에서 피규어와 장난감 구경하기\n" + - "placeName: 팝마트 명동 프리미엄 테마샵\n" + - "content: 동심으로 돌아가는 시간!\n" + - "keyword: ENTERTAINMENT\n" + - "\n" + - "**유의사항:**\n" + - "\n" + - "1. **`keyword`**는 반드시 한 개만 반환하며, 사용자가 입력한 활동 키워드 중에서만 선택합니다.\n" + - "2. **`placeName`** 은 반드시 활동 장소 또는 추천 장소의 이름(명사)의 형태로 제공합니다.\n" + - "3. 추천은 5개여야 합니다.\n" + - "4. 활동 소요 시간을 기준으로 추천 활동을 설계. spareTime으로 주어진 시간을 알차게 활용할 수 있는 활동만 제공합니다.\n" + - "5. **`title`**, **`placeName`**, **`content`**, **`keyword`**의 구조를 유지하면서도 내용이 중복되지 않도록 세부 사항을 차별화합니다"; - } + public static final String DEFAULT_SYSTEM_PROMPT = """ + 역할: + 너는 사용자가 입력한 정보를 바탕으로 자투리 시간에 할 수 있는 활동을 추천하는 AI 봇이야. 사용자가 제공하는 정보를 기반으로 적합한 활동을 5가지 추천해줘. 추천은 구체적이고 특정한걸로 되어야하며 추상적이거나 뻔한 활동은 배제해줘. + --- + 입력 정보: + 1. 자투리 시간: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등). + 2. 활동 타입: + - OFFLINE, ONLINE_AND_OFFLINE: 오프라인 활동 추천 + 3. 활동 키워드: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등). + 4. 장소 : 사용자 위치 + --- + 추천 기준: + 1. 활동 타입이 OFFLINE, ONLINE_AND_OFFLINE일 경우: + - 입력된 활동 키워드, 시간 그리고 장소를 고려하여 다양한 오프라인 활동을 추천. + - 추천되는 활동의 플랫폼은 한국 지역을 추천. + --- + 활동 키워드별 정의와 예시: + 1. SELF_DEVELOPMENT + - 정의: 시사상식, 지식, 교양과 관련된 활동으로, 개인의 성장과 발전을 위한 것 + - 예시: 뉴스 기사 읽기, 온라인 강연 보기, 팟캐스트 듣기, 언어 공부하기 등 + 2. ENTERTAINMENT + - 정의: 즐거움과 오락을 목적으로 한 활동, 순간의 재미와 유희를 위한 것 + - 예시: 유튜브 콘텐츠 시청하기, 음악듣기, OTT 시청하기 등 + 3. RELAXATION + - 정의: 신체적, 정신적 피로 회복과 재충전을 위한 정적인 활동 + - 예시: 명상하기, 짧은 글쓰기, ASMR 듣기 등 + 4. CULTURE_ART + - 정의: 예술적, 문화적 경험과 감상을 통해 영감과 인사이트를 얻는 활동 + - 예시: 버추얼 전시 감상하기, 예술 아티클 읽기, 문화예술 영상 보기 등 + 5. HEALTH + - 정의: 신체적, 정신적 건강을 개선하고 유지하기 위한 활동, 스포츠 중심 + - 예시: 스트레칭하기, 명상하기, 근력운동하기 등 + 6. SOCIAL + - 정의: 사회적 관계 형성과 유지를 위한 활동, 사람들과의 교류와 유대감 + - 예시: SNS 활동하기, 사람들과 소식 공유하기, 사람들에게 연락하기 등 + --- + 출력 형식: + 원하는 활동 타입 == OFFLINE, ONLINE_AND_OFFLINE: + - title: [활동 제목 또는 추천장소] + - content: [활동 부제목] + - keyword: [활동 키워드] + - placeName: 사용자 위치에 따른 추천 장소 + --- + 예시 입력과 출력: + 예시 (활동 타입 == OFFLINE || 활동 타입 == ONLINE_AND_OFFLINE) + 입력: + 자투리 시간: 20분 + 선호 활동 타입: OFFLINE + 활동 키워드: ENTERTAINMENT, CULTURE_ARTS + 장소: 서울시 강남구 + + 출력: + title: 서울도서관에서 인사이트 가득한 책 읽기 + placeName: 서울도서관 + content: 독서는 마음의 양식! + keyword: SELF_DEVELOPMENT + + 주의사항: + - 요청에서 오는 활동 키워드와 응답의 keyword를 매칭해서 알려줘. 요청 이외 키워드는 절대 넣지마 + - title, content, keyword, placeName 구조 이외는 아무런 문장이나 미사여구도 붙이지마 + - 총 5개 추천해줘 + """; +} From 28b0f08bed3440fc493af9b9705600afff7a0f4d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 14:37:57 +0900 Subject: [PATCH 346/478] =?UTF-8?q?refactor:=20(#123)=20Keyword=20Category?= =?UTF-8?q?=EC=99=80=20ImageUrl=20=EB=A7=A4=ED=95=91=EC=9D=84=20=EC=83=81?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=A7=8C=EB=93=A0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/value/Keyword.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 37990ec64..80a5fab79 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -16,6 +16,17 @@ @EqualsAndHashCode public class Keyword { + private static final Map categoryImageMap = Map.of( + Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", + Keyword.Category.HEALTH, "images/health.png", + Keyword.Category.NATURE, "images/nature.png", + Keyword.Category.CULTURE_ART, "images/culture_art.png", + Keyword.Category.ENTERTAINMENT, "images/entertainment.png", + Keyword.Category.RELAXATION, "images/relaxation.png", + Keyword.Category.SOCIAL, "images/social.png" + ); + + @Enumerated(EnumType.STRING) private Category category; @@ -47,18 +58,9 @@ public static Keyword create(Category category, String image) { } public static Keyword getKeywordByCategory(Category category) { - return Keyword.create(category, getCategoryImageMap().get(category)); - } - - private static Map getCategoryImageMap() { - return Map.of( - Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", - Keyword.Category.HEALTH, "images/health.png", - Keyword.Category.NATURE, "images/nature.png", - Keyword.Category.CULTURE_ART, "images/culture_art.png", - Keyword.Category.ENTERTAINMENT, "images/entertainment.png", - Keyword.Category.RELAXATION, "images/relaxation.png", - Keyword.Category.SOCIAL, "images/social.png" - ); + if (category == null) { + return null; + } + return Keyword.create(category, categoryImageMap.get(category)); } } From ecc76795b52fdabdfcc266de48d9ff6999951a1f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 14:42:15 +0900 Subject: [PATCH 347/478] =?UTF-8?q?refactor:=20(#123)=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8D=98=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=84=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=A1=9C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/activity/domain/value/Keyword.java | 1 - .../GetRecommendationsFromClovaService.java | 14 ++++++-------- .../GetRecommendationsFromOpenAIService.java | 12 +++++++----- .../dto/response/ClovaRecommendationResponse.java | 2 +- .../dto/response/OpenAIRecommendationResponse.java | 13 ++++++++----- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 80a5fab79..1307c614a 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -26,7 +26,6 @@ public class Keyword { Keyword.Category.SOCIAL, "images/social.png" ); - @Enumerated(EnumType.STRING) private Category category; diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 6a6655b14..5cce37889 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -65,7 +65,7 @@ public List getRecommendationsFromClova(AIRecommend private List filteredValidRecommendations(List clovaResponses) { return clovaResponses.stream() - .filter(clovaResponse -> clovaResponse.getKeywordCategory() != null && isValidKeywordCategory(clovaResponse.getKeywordCategory())).collect(Collectors.toList()); + .filter(clovaResponse -> clovaResponse.getKeyword() != null && isValidKeywordCategory(clovaResponse.getKeyword().getCategory())).collect(Collectors.toList()); } private List fetchRecommendations(AIRecommendationRequest clovaRecommendationRequest) { @@ -109,15 +109,13 @@ private List fetchRecommendations(AIRecommendationR i++; } - Keyword.Category keywordCategory = null; + Keyword keyword = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); - log.info("keywordText: {}", keywordText); - keywordCategory = convertClovaResponseKeywordToKeywordCategory(keywordText); - log.info("keywordCategory: {}", keywordCategory); + keyword = Keyword.getKeywordByCategory(convertClovaResponseKeywordToKeywordCategory(keywordText)); i++; } - clovaResponses.add(new ClovaRecommendationResponse(order, title, placeName, mapx, mapy, placeUrl, content, keywordCategory)); + clovaResponses.add(new ClovaRecommendationResponse(order, title, placeName, mapx, mapy, placeUrl, content, keyword)); order++; } } @@ -147,8 +145,8 @@ private void validateClovaResponse(ClovaResponse clovaResponse) { private boolean containsInvalidKeyword(List clovaResponses) { return clovaResponses.stream().anyMatch(clovaResponse -> - clovaResponse.getKeywordCategory() == null - || !isValidKeywordCategory(clovaResponse.getKeywordCategory())); + clovaResponse.getKeyword() == null + || !isValidKeywordCategory(clovaResponse.getKeyword().getCategory())); } private boolean isValidKeywordCategory(Keyword.Category keywordCategory) { diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java index a106d19e5..148ed704d 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java @@ -4,6 +4,7 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.domain.value.Type; import spring.backend.recommendation.dto.request.AIRecommendationRequest; @@ -96,8 +97,8 @@ private List parseRecommendations(String rawData, } if (hasAllRequiredFields(recommendationFields)) { - String keyword = recommendationFields.get(KEYWORD_KEY); - Category category = Category.from(keyword); + String keywordText = recommendationFields.get(KEYWORD_KEY); + Category category = Category.from(keywordText); if (isInvalidCategory(category)) { log.warn("[GetRecommendationsFromOpenAIService] Invalid Category."); @@ -108,13 +109,14 @@ private List parseRecommendations(String rawData, String title = recommendationFields.get(TITLE_KEY); String platform = recommendationFields.get(PLATFORM_KEY); String url = recommendationFields.get(URL_KEY); + Keyword keyword = Keyword.getKeywordByCategory(category); String youtubeUrl = processYoutubeUrl(title, platform, url); recommendations.add(OpenAIRecommendationResponse.of( order++, title, recommendationFields.get(CONTENT_KEY), - category, + keyword, youtubeUrl )); @@ -127,7 +129,7 @@ private List parseRecommendations(String rawData, private List filterAndLimitRecommendations(List recommendations, Type activityType) { return recommendations.stream() - .filter(r -> r.keywordCategory() != null) + .filter(r -> r.keyword() != null) .limit(getRequiredSize(activityType)) .collect(Collectors.toList()); } @@ -146,7 +148,7 @@ private boolean isInvalidCategory(Category category) { private boolean isKeywordMissing(List recommendations) { return recommendations.stream() - .anyMatch(r -> isInvalidCategory(r.keywordCategory())); + .anyMatch(r -> isInvalidCategory(r.keyword().getCategory())); } private boolean hasAllRequiredFields(Map recommendationFields) { diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java index 59c4e4499..7cce4febf 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java @@ -23,5 +23,5 @@ public class ClovaRecommendationResponse { @Schema(description = "추천 부제목") private String content; @Schema(description = "추천 키워드") - private Keyword.Category keywordCategory; + private Keyword keyword; } diff --git a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java index 9585003f6..c5a9a791c 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java @@ -1,7 +1,7 @@ package spring.backend.recommendation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import spring.backend.activity.domain.value.Keyword.Category; +import spring.backend.activity.domain.value.Keyword; public record OpenAIRecommendationResponse( @@ -14,13 +14,16 @@ public record OpenAIRecommendationResponse( @Schema(description = "꾸밈글", example = "휴식에는 역시 명상이 최고!") String content, - @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") - Category keywordCategory, + @Schema(description = "활동 키워드", example = "{\n" + + " \"category\": \"SELF_DEVELOPMENT\",\n" + + " \"image\": \"images/self_development.png\"\n" + + " }") + Keyword keyword, @Schema(description = "외부 링크", example = "https://www.youtube.com") String url ) { - public static OpenAIRecommendationResponse of(int order, String title, String content, Category category, String url) { - return new OpenAIRecommendationResponse(order, title, content, category, url); + public static OpenAIRecommendationResponse of(int order, String title, String content, Keyword keyword, String url) { + return new OpenAIRecommendationResponse(order, title, content, keyword, url); } } From 84ebe588c872a63cbdc326455c82b0caf578b6ce Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 21:50:57 +0900 Subject: [PATCH 348/478] =?UTF-8?q?feat:=20(#124)=20Member=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=B3=80=EA=B2=BD=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/member/domain/entity/Member.java | 13 +++++++++++++ .../backend/member/exception/MemberErrorCode.java | 4 +++- .../persistence/jpa/entity/MemberJpaEntity.java | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 880adcb56..c07318b57 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -31,6 +31,9 @@ public class Member { private String profileImage; + @Builder.Default + private boolean emailNotification = true; + private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -47,6 +50,7 @@ public static Member toDomainEntity(MemberJpaEntity memberJpaEntity) { .birthYear(memberJpaEntity.getBirthYear()) .gender(memberJpaEntity.getGender()) .profileImage(memberJpaEntity.getProfileImage()) + .emailNotification(memberJpaEntity.isEmailNotification()) .createdAt(memberJpaEntity.getCreatedAt()) .updatedAt(memberJpaEntity.getUpdatedAt()) .deleted(memberJpaEntity.getDeleted()) @@ -72,6 +76,15 @@ public void convertGuestToMember(String nickname, int birthYear, Gender gender, this.profileImage = profileImage; } + public void changeEmailNotification(boolean isEmailNotification) { + if (this.emailNotification == isEmailNotification) { + throw isEmailNotification + ? MemberErrorCode.ALREADY_ENABLE_EMAIL_NOTIFICATION.toException() + : MemberErrorCode.ALREADY_DISABLE_EMAIL_NOTIFICATION.toException(); + } + this.emailNotification = isEmailNotification; + } + public static Member createGuestMember(Provider provider, String email, String nickname) { return Member.builder() .provider(provider) diff --git a/src/main/java/spring/backend/member/exception/MemberErrorCode.java b/src/main/java/spring/backend/member/exception/MemberErrorCode.java index 7fd3cf93b..9e05748f3 100644 --- a/src/main/java/spring/backend/member/exception/MemberErrorCode.java +++ b/src/main/java/spring/backend/member/exception/MemberErrorCode.java @@ -19,7 +19,9 @@ public enum MemberErrorCode implements BaseErrorCode { INVALID_NICKNAME_LENGTH(HttpStatus.BAD_REQUEST, "닉네임은 1자에서 6자 사이여야 합니다."), INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "닉네임은 한글, 영문, 숫자 조합이어야 합니다."), ALREADY_REGISTERED_NICKNAME(HttpStatus.BAD_REQUEST, "닉네임이 이미 사용 중입니다."), - NOT_AUTHORIZED_MEMBER(HttpStatus.FORBIDDEN, "회원가입을 완료한 사용자가 아닙니다."); + NOT_AUTHORIZED_MEMBER(HttpStatus.FORBIDDEN, "회원가입을 완료한 사용자가 아닙니다."), + ALREADY_ENABLE_EMAIL_NOTIFICATION(HttpStatus.BAD_REQUEST, "이메일 알림이 이미 활성화되어 있습니다."), + ALREADY_DISABLE_EMAIL_NOTIFICATION(HttpStatus.BAD_REQUEST, "이메일 알림이 이미 비활성화되어 있습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java index 5a6254b20..6bb536107 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/entity/MemberJpaEntity.java @@ -40,6 +40,8 @@ public class MemberJpaEntity extends BaseEntity { private String profileImage; + private boolean emailNotification; + public static MemberJpaEntity toJpaEntity(Member member) { return MemberJpaEntity.builder() .id(member.getId()) @@ -50,6 +52,7 @@ public static MemberJpaEntity toJpaEntity(Member member) { .birthYear(member.getBirthYear()) .gender(member.getGender()) .profileImage(member.getProfileImage()) + .emailNotification(member.isEmailNotification()) .createdAt(member.getCreatedAt()) .updatedAt(member.getUpdatedAt()) .deleted(Optional.ofNullable(member.getDeleted()).orElse(false)) From 9879fa3429a4467acaff6ffe8eb6022accf1870d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 21:54:22 +0900 Subject: [PATCH 349/478] =?UTF-8?q?feat:=20(#124)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20API?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeEmailNotificationService.java | 21 ++++++++++++++++ .../ChangeEmailNotificationController.java | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/main/java/spring/backend/member/domain/service/ChangeEmailNotificationService.java create mode 100644 src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java diff --git a/src/main/java/spring/backend/member/domain/service/ChangeEmailNotificationService.java b/src/main/java/spring/backend/member/domain/service/ChangeEmailNotificationService.java new file mode 100644 index 000000000..502b05e97 --- /dev/null +++ b/src/main/java/spring/backend/member/domain/service/ChangeEmailNotificationService.java @@ -0,0 +1,21 @@ +package spring.backend.member.domain.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional +public class ChangeEmailNotificationService { + + private final MemberRepository memberRepository; + + public void changeEmailNotification(Member member) { + boolean isEmailNotificationEnabled = member.isEmailNotification(); + member.changeEmailNotification(!isEmailNotificationEnabled); + memberRepository.save(member); + } +} diff --git a/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java b/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java new file mode 100644 index 000000000..7be736817 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java @@ -0,0 +1,24 @@ +package spring.backend.member.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.service.ChangeEmailNotificationService; + +@RestController +@RequiredArgsConstructor +public class ChangeEmailNotificationController { + + private final ChangeEmailNotificationService changeEmailNotificationService; + + @Authorization + @PatchMapping("/v1/members/email-notification") + public ResponseEntity changeEmailNotification(@AuthorizedMember Member member) { + changeEmailNotificationService.changeEmailNotification(member); + return ResponseEntity.ok().build(); + } +} From 5b03f506a22e7420d370f94dee9f8131fb2303f0 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 21:58:08 +0900 Subject: [PATCH 350/478] =?UTF-8?q?feat:=20(#124)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20API=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeEmailNotificationController.java | 3 ++- .../ChangeEmailNotificationSwagger.java | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/java/spring/backend/member/presentation/swagger/ChangeEmailNotificationSwagger.java diff --git a/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java b/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java index 7be736817..7437e0ac1 100644 --- a/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java +++ b/src/main/java/spring/backend/member/presentation/ChangeEmailNotificationController.java @@ -8,10 +8,11 @@ import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.service.ChangeEmailNotificationService; +import spring.backend.member.presentation.swagger.ChangeEmailNotificationSwagger; @RestController @RequiredArgsConstructor -public class ChangeEmailNotificationController { +public class ChangeEmailNotificationController implements ChangeEmailNotificationSwagger { private final ChangeEmailNotificationService changeEmailNotificationService; diff --git a/src/main/java/spring/backend/member/presentation/swagger/ChangeEmailNotificationSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ChangeEmailNotificationSwagger.java new file mode 100644 index 000000000..34ea2a850 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/swagger/ChangeEmailNotificationSwagger.java @@ -0,0 +1,22 @@ +package spring.backend.member.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.exception.MemberErrorCode; + +@Tag(name = "Member", description = "멤버") +public interface ChangeEmailNotificationSwagger { + + @Operation( + summary = "이메일 알림 설정 API", + description = "사용자의 이메일 알림 설정을 변경합니다. \n\n 기본적으로 이메일 알림은 활성화되어 있으며, 요청을 통해 활성화 또는 비활성화할 수 있습니다.", + operationId = "/v1/members/email-notification" + ) + @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) + ResponseEntity changeEmailNotification(@Parameter(hidden = true) Member member); +} From b400f93393194e8a0c227d12574d305680b0ab67 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sat, 23 Nov 2024 22:14:29 +0900 Subject: [PATCH 351/478] =?UTF-8?q?feat:=20(#124)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/domain/entity/MemberTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/spring/backend/member/domain/entity/MemberTest.java diff --git a/src/test/java/spring/backend/member/domain/entity/MemberTest.java b/src/test/java/spring/backend/member/domain/entity/MemberTest.java new file mode 100644 index 000000000..829b00043 --- /dev/null +++ b/src/test/java/spring/backend/member/domain/entity/MemberTest.java @@ -0,0 +1,38 @@ +package spring.backend.member.domain.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import spring.backend.core.exception.DomainException; +import spring.backend.member.exception.MemberErrorCode; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MemberTest { + + @DisplayName("이메일 알림이 이미 활성화된 상태에서 다시 활성화하려고 하면 예외가 발생한다.") + @Test + void changeEmailNotification_throwsException_whenAlreadyEnabled() { + // Given + Member member = Member.builder() + .emailNotification(true) + .build(); + + // Then + DomainException ex = assertThrows(DomainException.class, () -> member.changeEmailNotification(true)); + assertEquals(MemberErrorCode.ALREADY_ENABLE_EMAIL_NOTIFICATION.getMessage(), ex.getMessage()); + } + + @DisplayName("이메일 알림이 이미 비활성화된 상태에서 다시 비활성화하려고 하면 예외가 발생한다.") + @Test + void changeEmailNotification_throwsException_whenAlreadyDisabled() { + // Given + Member member = Member.builder() + .emailNotification(false) + .build(); + + // Then + DomainException ex = assertThrows(DomainException.class, () -> member.changeEmailNotification(false)); + assertEquals(MemberErrorCode.ALREADY_DISABLE_EMAIL_NOTIFICATION.getMessage(), ex.getMessage()); + } +} From 31556c7d4bdc41233cff6fd7dafd0d157fd547ca Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 20:53:28 +0900 Subject: [PATCH 352/478] =?UTF-8?q?feat:=20(#126)=20Member=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=EC=97=90=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/spring/backend/member/domain/entity/Member.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index c07318b57..392dc6d70 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -93,4 +93,9 @@ public static Member createGuestMember(Provider provider, String email, String n .nickname(nickname) .build(); } + + public void editMemberProfile(String nickname, String profileImage) { + this.nickname = nickname; + this.profileImage = profileImage; + } } From 7a0b91d217b4c58197c15dd2396e272b8a528d58 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 20:54:02 +0900 Subject: [PATCH 353/478] =?UTF-8?q?feat:=20(#126)=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=EC=99=80=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditMemberProfileController.java | 28 +++++++++++++++++++ .../swagger/EditMemberProfileSwagger.java | 24 ++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/spring/backend/member/presentation/EditMemberProfileController.java create mode 100644 src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java diff --git a/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java new file mode 100644 index 000000000..4f9420216 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java @@ -0,0 +1,28 @@ +package spring.backend.member.presentation; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.member.domain.service.EditMemberProfileService; +import spring.backend.member.presentation.swagger.EditMemberProfileSwagger; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class EditMemberProfileController implements EditMemberProfileSwagger { + + private final EditMemberProfileService editMemberProfileService; + + @Override + @PatchMapping("/v1/member/profile") + @Authorization + public ResponseEntity> editMemberProfile(@AuthorizedMember Member member, @Valid @RequestBody EditMemberProfileRequest editMemberProfileRequest) { + boolean isProfileChanged = editMemberProfileService.edit(member, editMemberProfileRequest); + return ResponseEntity.ok(new RestResponse<>(isProfileChanged)); + } +} diff --git a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java new file mode 100644 index 000000000..e787a16f0 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java @@ -0,0 +1,24 @@ +package spring.backend.member.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.exception.MemberErrorCode; + +@Tag(name = "Member", description = "멤버") +public interface EditMemberProfileSwagger { + + @Operation( + summary = "멤버 프로필 수정 API", + description = "사용자의 프로필을 수정합니다.", + operationId = "/v1/member/profile" + ) + @ApiErrorCode({GlobalErrorCode.class}) + ResponseEntity> editMemberProfile(@Parameter(hidden = true) Member member, EditMemberProfileRequest request); +} From 7aed7dcb7ebbd706a8972c0d0320a16a17febcf2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 20:54:18 +0900 Subject: [PATCH 354/478] =?UTF-8?q?feat:=20(#126)=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=A6=AC=ED=80=98=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20DTO=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/EditMemberProfileRequest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java diff --git a/src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java b/src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java new file mode 100644 index 000000000..3fb736200 --- /dev/null +++ b/src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java @@ -0,0 +1,17 @@ +package spring.backend.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record EditMemberProfileRequest( + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,6}$", message = "닉네임은 한글, 영문, 숫자 조합 6자 이내로 입력해주세요.") + @NotBlank(message = "닉네임을 입력해주세요.") + @Schema(description = "닉네임", example = "조각조각") + String nickname, + + @NotBlank(message = "프로필 이미지를 선택해주세요.") + @Schema(description = "프로필 이미지", example = "http://test.jpg") + String profileImage +) { +} From e9c54d2a18b1df550582872ad54cbf61ae7ec187 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 20:54:50 +0900 Subject: [PATCH 355/478] =?UTF-8?q?feat:=20(#126)=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/EditMemberProfileService.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java new file mode 100644 index 000000000..355dabb0f --- /dev/null +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -0,0 +1,52 @@ +package spring.backend.member.domain.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.exception.MemberErrorCode; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class EditMemberProfileService { + + private final MemberRepository memberRepository; + + public boolean edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { + try { + if (validateNickname(editMemberProfileRequest.nickname())) { + member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); + memberRepository.save(member); + return true; + } else { + return false; + } + } catch (Exception e) { + log.error("[EditMemberProfileService] Failed to edit member profile. {}", e.getMessage()); + throw MemberErrorCode.FAILED_TO_EDIT_PROFILE.toException(); + } + } + + private boolean validateNickname(String nickname) { + if (nickname == null || nickname.isBlank()) { + log.error("[ValidateNicknameService] Nickname is empty"); + return false; + } + + if (nickname.length() > 6) { + log.error("[ValidateNicknameService] Nickname is smaller than 6 characters"); + return false; + } + + if (!nickname.matches("^[a-zA-Z0-9가-힣]+$")) { + log.error("[ValidateNicknameService] Nickname is invalid"); + return false; + } + + return true; + } +} + From 6d6241e2a6711a7d9a1e0232832f5606602daabe Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 20:55:00 +0900 Subject: [PATCH 356/478] =?UTF-8?q?feat:=20(#126)=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditMemberProfileServiceTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java diff --git a/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java b/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java new file mode 100644 index 000000000..d9d2b6d1f --- /dev/null +++ b/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java @@ -0,0 +1,64 @@ +package spring.backend.member.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.service.EditMemberProfileService; +import spring.backend.member.domain.value.Role; +import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.exception.MemberErrorCode; + +import static org.junit.jupiter.api.Assertions.*; + +public class EditMemberProfileServiceTest { + @InjectMocks + private EditMemberProfileService editMemberProfileService; + + private Member member; + + @BeforeEach + void setUp() { + member = Member.builder() + .role(Role.MEMBER) + .build(); + + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("닉네임이 공백일 때 예외가 발생한다.") + void throwExceptionWhenNicknameIsBlank() { + // Given + String nickname = " "; + EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); + + // When & Then + assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); + } + + @Test + @DisplayName("닉네임 길이가 6자를 초과할 때 예외가 발생한다.") + void throwExceptionWhenNicknameLengthIsInvalid() { + // Given + String nickname = "1234567"; + EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); + + // When & Then + assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); + } + + @Test + @DisplayName("닉네임 형식이 유효하지 않을 때 예외가 발생한다.") + void throwExceptionWhenNicknameFormatIsInvalid() { + // Given + String nickname = "조각ㅈㄱ"; + EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); + + // When & Then + assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); + } +} From 0af49f6049f4e181ff966cff8a8a56265e85a183 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 22:17:33 +0900 Subject: [PATCH 357/478] =?UTF-8?q?refactor:=20(#126)=20EditMemberProfileS?= =?UTF-8?q?wagger=EC=97=90=20MemberErrorCode=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/presentation/swagger/EditMemberProfileSwagger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java index e787a16f0..ed29c569a 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java @@ -19,6 +19,6 @@ public interface EditMemberProfileSwagger { description = "사용자의 프로필을 수정합니다.", operationId = "/v1/member/profile" ) - @ApiErrorCode({GlobalErrorCode.class}) + @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) ResponseEntity> editMemberProfile(@Parameter(hidden = true) Member member, EditMemberProfileRequest request); } From f0d38e05e8e0a9133dca185f8f3b4d7be5e08c31 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 22:18:16 +0900 Subject: [PATCH 358/478] =?UTF-8?q?feat:=20(#126)=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20ValidateNicknameService=EC=97=90=20=EC=9C=84?= =?UTF-8?q?=EC=9E=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/EditMemberProfileService.java | 36 ++--------- .../EditMemberProfileServiceTest.java | 64 ------------------- 2 files changed, 5 insertions(+), 95 deletions(-) delete mode 100644 src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 355dabb0f..6da654692 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; +import spring.backend.member.application.ValidateNicknameService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.dto.request.EditMemberProfileRequest; @@ -14,39 +15,12 @@ public class EditMemberProfileService { private final MemberRepository memberRepository; + private final ValidateNicknameService validateNicknameService; public boolean edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { - try { - if (validateNickname(editMemberProfileRequest.nickname())) { - member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); - memberRepository.save(member); - return true; - } else { - return false; - } - } catch (Exception e) { - log.error("[EditMemberProfileService] Failed to edit member profile. {}", e.getMessage()); - throw MemberErrorCode.FAILED_TO_EDIT_PROFILE.toException(); - } - } - - private boolean validateNickname(String nickname) { - if (nickname == null || nickname.isBlank()) { - log.error("[ValidateNicknameService] Nickname is empty"); - return false; - } - - if (nickname.length() > 6) { - log.error("[ValidateNicknameService] Nickname is smaller than 6 characters"); - return false; - } - - if (!nickname.matches("^[a-zA-Z0-9가-힣]+$")) { - log.error("[ValidateNicknameService] Nickname is invalid"); - return false; - } - + validateNicknameService.validateNickname(editMemberProfileRequest.nickname()); + member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); + memberRepository.save(member); return true; } } - diff --git a/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java b/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java deleted file mode 100644 index d9d2b6d1f..000000000 --- a/src/test/java/spring/backend/member/application/EditMemberProfileServiceTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package spring.backend.member.application; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; -import spring.backend.core.exception.DomainException; -import spring.backend.member.domain.entity.Member; -import spring.backend.member.domain.service.EditMemberProfileService; -import spring.backend.member.domain.value.Role; -import spring.backend.member.dto.request.EditMemberProfileRequest; -import spring.backend.member.exception.MemberErrorCode; - -import static org.junit.jupiter.api.Assertions.*; - -public class EditMemberProfileServiceTest { - @InjectMocks - private EditMemberProfileService editMemberProfileService; - - private Member member; - - @BeforeEach - void setUp() { - member = Member.builder() - .role(Role.MEMBER) - .build(); - - MockitoAnnotations.openMocks(this); - } - - @Test - @DisplayName("닉네임이 공백일 때 예외가 발생한다.") - void throwExceptionWhenNicknameIsBlank() { - // Given - String nickname = " "; - EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); - - // When & Then - assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); - } - - @Test - @DisplayName("닉네임 길이가 6자를 초과할 때 예외가 발생한다.") - void throwExceptionWhenNicknameLengthIsInvalid() { - // Given - String nickname = "1234567"; - EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); - - // When & Then - assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); - } - - @Test - @DisplayName("닉네임 형식이 유효하지 않을 때 예외가 발생한다.") - void throwExceptionWhenNicknameFormatIsInvalid() { - // Given - String nickname = "조각ㅈㄱ"; - EditMemberProfileRequest editMemberProfileRequest = new EditMemberProfileRequest(nickname, "profileImage"); - - // When & Then - assertFalse(editMemberProfileService.edit(member, editMemberProfileRequest)); - } -} From 2b5357755d88dfec26ef91963f54add07fcdc05d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 22:21:03 +0900 Subject: [PATCH 359/478] =?UTF-8?q?feat:=20(#126)=20EditMemberProfileServi?= =?UTF-8?q?ce=20edit=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20@Transaction?= =?UTF-8?q?al=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/service/EditMemberProfileService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 6da654692..4aefb31f8 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import spring.backend.member.application.ValidateNicknameService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; @@ -17,6 +18,7 @@ public class EditMemberProfileService { private final MemberRepository memberRepository; private final ValidateNicknameService validateNicknameService; + @Transactional public boolean edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { validateNicknameService.validateNickname(editMemberProfileRequest.nickname()); member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); From b6d4ab35cb4307c6047d32bc4c11bc6cb6c020de Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 22:33:29 +0900 Subject: [PATCH 360/478] =?UTF-8?q?feat:=20(#126)=20EditMemberProfile?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=ED=99=98=EA=B0=92=EC=9D=84=20void?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/domain/service/EditMemberProfileService.java | 3 +-- .../member/presentation/EditMemberProfileController.java | 9 ++++----- .../presentation/swagger/EditMemberProfileSwagger.java | 7 ++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 4aefb31f8..867ff4d46 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -19,10 +19,9 @@ public class EditMemberProfileService { private final ValidateNicknameService validateNicknameService; @Transactional - public boolean edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { + public void edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { validateNicknameService.validateNickname(editMemberProfileRequest.nickname()); member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); memberRepository.save(member); - return true; } } diff --git a/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java index 4f9420216..89807571e 100644 --- a/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java +++ b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java @@ -2,9 +2,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import spring.backend.core.presentation.RestResponse; import spring.backend.member.dto.request.EditMemberProfileRequest; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; @@ -21,8 +20,8 @@ public class EditMemberProfileController implements EditMemberProfileSwagger { @Override @PatchMapping("/v1/member/profile") @Authorization - public ResponseEntity> editMemberProfile(@AuthorizedMember Member member, @Valid @RequestBody EditMemberProfileRequest editMemberProfileRequest) { - boolean isProfileChanged = editMemberProfileService.edit(member, editMemberProfileRequest); - return ResponseEntity.ok(new RestResponse<>(isProfileChanged)); + @ResponseStatus(HttpStatus.OK) + public void editMemberProfile(@AuthorizedMember Member member, @Valid @RequestBody EditMemberProfileRequest editMemberProfileRequest) { + editMemberProfileService.edit(member, editMemberProfileRequest); } } diff --git a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java index ed29c569a..bde0687f2 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java @@ -3,13 +3,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; import spring.backend.member.dto.request.EditMemberProfileRequest; -import spring.backend.member.exception.MemberErrorCode; @Tag(name = "Member", description = "멤버") public interface EditMemberProfileSwagger { @@ -19,6 +16,6 @@ public interface EditMemberProfileSwagger { description = "사용자의 프로필을 수정합니다.", operationId = "/v1/member/profile" ) - @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) - ResponseEntity> editMemberProfile(@Parameter(hidden = true) Member member, EditMemberProfileRequest request); + @ApiErrorCode({GlobalErrorCode.class}) + void editMemberProfile(@Parameter(hidden = true) Member member, EditMemberProfileRequest request); } From e6055cc03d7892c991e21bb6c988a8392f798600 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 23:40:32 +0900 Subject: [PATCH 361/478] =?UTF-8?q?feat:=20(#126)=20EditMemberProfile?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=ED=99=98=EA=B0=92=EC=9D=84=20void?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/presentation/swagger/EditMemberProfileSwagger.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java index bde0687f2..d091b00f2 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java @@ -7,6 +7,7 @@ import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.member.domain.entity.Member; import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.exception.MemberErrorCode; @Tag(name = "Member", description = "멤버") public interface EditMemberProfileSwagger { @@ -16,6 +17,6 @@ public interface EditMemberProfileSwagger { description = "사용자의 프로필을 수정합니다.", operationId = "/v1/member/profile" ) - @ApiErrorCode({GlobalErrorCode.class}) + @ApiErrorCode({GlobalErrorCode.class, MemberErrorCode.class}) void editMemberProfile(@Parameter(hidden = true) Member member, EditMemberProfileRequest request); } From ad58cdb31a55e299d31fbaccc8008a0c4b270874 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 24 Nov 2024 02:45:33 +0900 Subject: [PATCH 362/478] =?UTF-8?q?refactor:=20(#126)=20EditMemberProfileS?= =?UTF-8?q?ervice=EC=9D=98=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/service/EditMemberProfileService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 867ff4d46..0fe096b0c 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -16,11 +16,9 @@ public class EditMemberProfileService { private final MemberRepository memberRepository; - private final ValidateNicknameService validateNicknameService; @Transactional public void edit(Member member, EditMemberProfileRequest editMemberProfileRequest) { - validateNicknameService.validateNickname(editMemberProfileRequest.nickname()); member.editMemberProfile(editMemberProfileRequest.nickname(), editMemberProfileRequest.profileImage()); memberRepository.save(member); } From f2619c3c7b112e6cae5f2e3c17a8e516459e47ec Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 02:42:43 +0900 Subject: [PATCH 363/478] =?UTF-8?q?feat:=20(#133)=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20url=EC=9D=84=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94?= =?UTF-8?q?=20Property=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/property/ImageProperty.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/property/ImageProperty.java diff --git a/src/main/java/spring/backend/core/configuration/property/ImageProperty.java b/src/main/java/spring/backend/core/configuration/property/ImageProperty.java new file mode 100644 index 000000000..ae66ccccf --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/property/ImageProperty.java @@ -0,0 +1,20 @@ +package spring.backend.core.configuration.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties("image") +public class ImageProperty { + + private String selfDevelopmentImageUrl; + private String healthImageUrl; + private String cultureArtImageUrl; + private String entertainmentImageUrl; + private String relaxationImageUrl; + private String socialImageUrl; +} From d9d76cf99fe03f88cd586ea83e7fb904cbb9cb8a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 02:44:51 +0900 Subject: [PATCH 364/478] =?UTF-8?q?feat:=20(#133)=20Category=EB=A1=9C=20Im?= =?UTF-8?q?age=EB=A5=BC=20=EB=B3=80=ED=99=98=ED=95=98=EB=8A=94=20Converter?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/value/Keyword.java | 17 --------- .../core/converter/ImageConverter.java | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 src/main/java/spring/backend/core/converter/ImageConverter.java diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 1307c614a..104e92564 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -16,16 +16,6 @@ @EqualsAndHashCode public class Keyword { - private static final Map categoryImageMap = Map.of( - Keyword.Category.SELF_DEVELOPMENT, "images/self_development.png", - Keyword.Category.HEALTH, "images/health.png", - Keyword.Category.NATURE, "images/nature.png", - Keyword.Category.CULTURE_ART, "images/culture_art.png", - Keyword.Category.ENTERTAINMENT, "images/entertainment.png", - Keyword.Category.RELAXATION, "images/relaxation.png", - Keyword.Category.SOCIAL, "images/social.png" - ); - @Enumerated(EnumType.STRING) private Category category; @@ -55,11 +45,4 @@ public static Category from(String description) { public static Keyword create(Category category, String image) { return new Keyword(category, image); } - - public static Keyword getKeywordByCategory(Category category) { - if (category == null) { - return null; - } - return Keyword.create(category, categoryImageMap.get(category)); - } } diff --git a/src/main/java/spring/backend/core/converter/ImageConverter.java b/src/main/java/spring/backend/core/converter/ImageConverter.java new file mode 100644 index 000000000..c935bfd35 --- /dev/null +++ b/src/main/java/spring/backend/core/converter/ImageConverter.java @@ -0,0 +1,35 @@ +package spring.backend.core.converter; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import spring.backend.activity.domain.value.Keyword.Category; +import spring.backend.core.configuration.property.ImageProperty; + +import java.util.HashMap; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class ImageConverter { + + private final ImageProperty imageProperty; + private final Map categoryImageMap = new HashMap<>(); + + @PostConstruct + private void initializeImageMap() { + categoryImageMap.put(Category.SELF_DEVELOPMENT, imageProperty.getSelfDevelopmentImageUrl()); + categoryImageMap.put(Category.HEALTH, imageProperty.getHealthImageUrl()); + categoryImageMap.put(Category.CULTURE_ART, imageProperty.getCultureArtImageUrl()); + categoryImageMap.put(Category.ENTERTAINMENT, imageProperty.getEntertainmentImageUrl()); + categoryImageMap.put(Category.RELAXATION, imageProperty.getRelaxationImageUrl()); + categoryImageMap.put(Category.SOCIAL, imageProperty.getSocialImageUrl()); + } + + public String convertToImageUrl(Category category) { + if (category == null) { + return null; + } + return categoryImageMap.get(category); + } +} From c72cb8bceaa60cc922193a45c39e751c0bdaaee1 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 02:46:15 +0900 Subject: [PATCH 365/478] =?UTF-8?q?fix:=20(#133)=20ImageConverter=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=9D=B4=EC=A0=84=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...itiesByMemberAndKeywordInMonthService.java | 4 +++- .../GetRecommendationsFromClovaService.java | 20 +++++++++++-------- .../GetRecommendationsFromOpenAIService.java | 4 +++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index cf16b1248..a1049f43a 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -8,6 +8,7 @@ import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; import spring.backend.activity.dto.response.TotalSavedTimeAndActivityCountByKeywordInMonth; import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.core.converter.ImageConverter; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; @@ -20,6 +21,7 @@ @Transactional(readOnly = true) public class ReadActivitiesByMemberAndKeywordInMonthService { private final ActivityDao activityDao; + private final ImageConverter imageConverter; public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeywordInMonth(Member member, int year, int month, Keyword.Category keywordCategory) { YearMonth yearMonth = YearMonth.of(year, month); @@ -27,7 +29,7 @@ public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeyw LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); TotalSavedTimeAndActivityCountByKeywordInMonth totalSavedTimeAndActivityCountByKeywordInMonth = activityDao.findTotalSavedTimeAndActivityCountByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - Keyword keyword = Keyword.getKeywordByCategory(keywordCategory); + Keyword keyword = Keyword.create(keywordCategory, imageConverter.convertToImageUrl(keywordCategory)); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeAndActivityCountByKeywordInMonth.totalSavedTimeByKeywordInMonth(), totalSavedTimeAndActivityCountByKeywordInMonth.totalActivityCountByKeywordInMonth(), diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 5cce37889..f1ef678a7 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -4,7 +4,9 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.exception.ActivityErrorCode; +import spring.backend.core.converter.ImageConverter; import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; @@ -35,6 +37,7 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; private final PlaceInfoProvider kakaomapPlaceInfoProvider; + private final ImageConverter imageConverter; public List getRecommendationsFromClova(AIRecommendationRequest clovaRecommendationRequest) { validateLocation(clovaRecommendationRequest); @@ -112,7 +115,8 @@ private List fetchRecommendations(AIRecommendationR Keyword keyword = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); - keyword = Keyword.getKeywordByCategory(convertClovaResponseKeywordToKeywordCategory(keywordText)); + Category category = convertClovaResponseKeywordToKeywordCategory(keywordText); + keyword = Keyword.create(category, imageConverter.convertToImageUrl(category)); i++; } clovaResponses.add(new ClovaRecommendationResponse(order, title, placeName, mapx, mapy, placeUrl, content, keyword)); @@ -149,26 +153,26 @@ private boolean containsInvalidKeyword(List clovaRe || !isValidKeywordCategory(clovaResponse.getKeyword().getCategory())); } - private boolean isValidKeywordCategory(Keyword.Category keywordCategory) { - return Arrays.stream(Keyword.Category.values()).anyMatch(category -> category == keywordCategory); + private boolean isValidKeywordCategory(Category keywordCategory) { + return Arrays.stream(Category.values()).anyMatch(category -> category == keywordCategory); } private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest clovaRecommendationRequest) { - if (clovaRecommendationRequest.activityType().equals(ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.NATURE) + if (clovaRecommendationRequest.activityType().equals(ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Category.NATURE) ) { throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); } - if (clovaRecommendationRequest.activityType().equals(OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Keyword.Category.SOCIAL) + if (clovaRecommendationRequest.activityType().equals(OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Category.SOCIAL) ) { throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); } } - private Keyword.Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { + private Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { try { - return Keyword.Category.valueOf(keywordText); + return Category.valueOf(keywordText); } catch (IllegalArgumentException e) { - return Arrays.stream(Keyword.Category.values()) + return Arrays.stream(Category.values()) .filter(category -> category.getDescription().equals(keywordText)) .findFirst() .orElse(null); diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java index 148ed704d..4c12fd120 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java @@ -7,6 +7,7 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.domain.value.Type; +import spring.backend.core.converter.ImageConverter; import spring.backend.recommendation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; import spring.backend.recommendation.infrastructure.dto.Message; @@ -39,6 +40,7 @@ public class GetRecommendationsFromOpenAIService { private final RecommendationProvider> openAIRecommendationProvider; private final SearchYouTubeService searchYouTubeService; + private final ImageConverter imageConverter; public List getRecommendationsFromOpenAI(AIRecommendationRequest request) { for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { @@ -109,7 +111,7 @@ private List parseRecommendations(String rawData, String title = recommendationFields.get(TITLE_KEY); String platform = recommendationFields.get(PLATFORM_KEY); String url = recommendationFields.get(URL_KEY); - Keyword keyword = Keyword.getKeywordByCategory(category); + Keyword keyword = Keyword.create(category, imageConverter.convertToImageUrl(category)); String youtubeUrl = processYoutubeUrl(title, platform, url); recommendations.add(OpenAIRecommendationResponse.of( From 7dafdfe592b97cb8d7ebf46864ca83bf1ad399d3 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 03:10:50 +0900 Subject: [PATCH 366/478] =?UTF-8?q?feat:=20(#133)=20ImageConverter=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/converter/ImageConverterTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/java/spring/backend/core/converter/ImageConverterTest.java diff --git a/src/test/java/spring/backend/core/converter/ImageConverterTest.java b/src/test/java/spring/backend/core/converter/ImageConverterTest.java new file mode 100644 index 000000000..0f5ffbd0a --- /dev/null +++ b/src/test/java/spring/backend/core/converter/ImageConverterTest.java @@ -0,0 +1,34 @@ +package spring.backend.core.converter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.activity.domain.value.Keyword.Category; +import spring.backend.core.configuration.property.ImageProperty; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SpringBootTest +class ImageConverterTest { + + @Autowired + private ImageConverter imageConverter; + + @Autowired + private ImageProperty imageProperty; + + @DisplayName("주어진 카테고리에 맞는 이미지 URL을 반환한다") + @Test + void mapCategoryToImageUrl() { + assertEquals(imageProperty.getSelfDevelopmentImageUrl(), imageConverter.convertToImageUrl(Category.SELF_DEVELOPMENT)); + assertEquals(imageProperty.getHealthImageUrl(), imageConverter.convertToImageUrl(Category.HEALTH)); + assertEquals(imageProperty.getCultureArtImageUrl(), imageConverter.convertToImageUrl(Category.CULTURE_ART)); + assertEquals(imageProperty.getEntertainmentImageUrl(), imageConverter.convertToImageUrl(Category.ENTERTAINMENT)); + assertEquals(imageProperty.getRelaxationImageUrl(), imageConverter.convertToImageUrl(Category.RELAXATION)); + assertEquals(imageProperty.getSocialImageUrl(), imageConverter.convertToImageUrl(Category.SOCIAL)); + + assertNull(imageConverter.convertToImageUrl(null)); + } +} From 29d4106dde2fff5453c851481fa5661340211a10 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 05:06:31 +0900 Subject: [PATCH 367/478] =?UTF-8?q?fix:=20(#135)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EA=B0=80=EC=9E=85=20=EB=82=A0=EC=A7=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 3 +-- .../auth/dto/response/LoginResponse.java | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 75d7cac4a..81305a7f6 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -53,7 +53,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - - return new LoginResponse(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member.getRole()); + return LoginResponse.of(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member.getRole(), member.getCreatedAt()); } } diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java index 5481466f4..7f3d3c6c5 100644 --- a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java +++ b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java @@ -1,6 +1,25 @@ package spring.backend.auth.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.member.domain.value.Role; -public record LoginResponse(String accessToken, String refreshToken, Role role) { +import java.time.LocalDateTime; + +public record LoginResponse( + + @Schema(description = "액세스 토큰") + String accessToken, + + @Schema(description = "리프레시 토큰") + String refreshToken, + + @Schema(description = "사용자 유형(MEMBER, GUEST)", example = "MEMBER") + Role role, + + @Schema(description = "사용자 가입 날짜", example = "2024-11-14T06:10:55.091954") + LocalDateTime registrationDate +) { + public static LoginResponse of(String accessToken, String refreshToken, Role role, LocalDateTime registrationDate) { + return new LoginResponse(accessToken, refreshToken, role, registrationDate); + } } From e1e6edd61e4a64340cbfd72f1637f92f2693d016 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 05:08:39 +0900 Subject: [PATCH 368/478] =?UTF-8?q?feat:=20(#135)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/OnboardingSignUpService.java | 6 ++-- .../response/OnboardingSignUpResponse.java | 31 +++++++++++++++++++ .../OnboardingSignUpController.java | 9 +++--- .../swagger/OnboardingSignUpSwagger.java | 5 ++- 4 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java diff --git a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java index d4fff3705..34d511a26 100644 --- a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java +++ b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; @@ -21,12 +22,13 @@ public class OnboardingSignUpService { private final MemberRepository memberRepository; - public Member onboardingSignUp(Member member, OnboardingSignUpRequest request) { + public OnboardingSignUpResponse onboardingSignUp(Member member, OnboardingSignUpRequest request) { validateMember(member); validateRequest(request); validateBirthYear(request); member.convertGuestToMember(request.nickname(), request.birthYear(), request.gender(), request.profileImage()); - return memberRepository.save(member); + memberRepository.save(member); + return OnboardingSignUpResponse.of(member.getEmail(), member.getNickname(), member.getBirthYear(), member.getGender(), member.getProfileImage(), member.getCreatedAt()); } private void validateMember(Member member) { diff --git a/src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java b/src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java new file mode 100644 index 000000000..dff23c107 --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java @@ -0,0 +1,31 @@ +package spring.backend.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.member.domain.value.Gender; + +import java.time.LocalDateTime; + +public record OnboardingSignUpResponse( + + @Schema(description = "사용자 이메일", example = "example@example.com") + String email, + + @Schema(description = "사용자 닉네임", example = "john_doe") + String nickname, + + @Schema(description = "사용자 출생 연도", example = "1990") + int birthYear, + + @Schema(description = "사용자 성별 (MALE, FEMALE, NONE)", example = "MALE") + Gender gender, + + @Schema(description = "사용자 프로필 이미지 URL", example = "https://example.com/profile.jpg") + String profileImage, + + @Schema(description = "사용자 가입 날짜", example = "2024-11-14T06:10:55.091954") + LocalDateTime registrationDate +){ + public static OnboardingSignUpResponse of(String email, String nickname, int birthYear, Gender gender, String profileImage, LocalDateTime registrationDate) { + return new OnboardingSignUpResponse(email, nickname, birthYear, gender, profileImage, registrationDate); + } +} diff --git a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java index 52d0541b3..73083bbe8 100644 --- a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java +++ b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java @@ -8,14 +8,13 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.OnboardingSignUpService; import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.dto.response.OnboardingSignUpResponse; import spring.backend.auth.presentation.swagger.OnboardingSignUpSwagger; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import java.util.UUID; - @RestController @RequiredArgsConstructor public class OnboardingSignUpController implements OnboardingSignUpSwagger { @@ -24,8 +23,8 @@ public class OnboardingSignUpController implements OnboardingSignUpSwagger { @Authorization @PostMapping("/v1/members/onboard") - public ResponseEntity> onboardingSignUp(@LoginMember Member member, @Valid @RequestBody OnboardingSignUpRequest request) { - Member updatedMember = onboardingSignUpService.onboardingSignUp(member, request); - return ResponseEntity.ok(new RestResponse<>(updatedMember.getId())); + public ResponseEntity> onboardingSignUp(@LoginMember Member member, @Valid @RequestBody OnboardingSignUpRequest request) { + OnboardingSignUpResponse onboardingSignUpResponse = onboardingSignUpService.onboardingSignUp(member, request); + return ResponseEntity.ok(new RestResponse<>(onboardingSignUpResponse)); } } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java index 676ce67b2..03c4cf40a 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; @@ -12,8 +13,6 @@ import spring.backend.member.domain.entity.Member; import spring.backend.member.exception.MemberErrorCode; -import java.util.UUID; - @Tag(name = "Auth", description = "인증/인가") public interface OnboardingSignUpSwagger { @@ -23,5 +22,5 @@ public interface OnboardingSignUpSwagger { operationId = "/v1/members/onboard" ) @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class, MemberErrorCode.class}) - ResponseEntity> onboardingSignUp(@Parameter(hidden = true) Member member, OnboardingSignUpRequest request); + ResponseEntity> onboardingSignUp(@Parameter(hidden = true) Member member, OnboardingSignUpRequest request); } From fd4768a2f30a43c946306c75a177feec9cce4a5f Mon Sep 17 00:00:00 2001 From: anxi01 Date: Sun, 24 Nov 2024 05:13:55 +0900 Subject: [PATCH 369/478] =?UTF-8?q?fix:=20(#135)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EA=B0=80=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/OnboardingSignUpServiceTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java index a85ca4705..7ef70d723 100644 --- a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java +++ b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java @@ -7,6 +7,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import spring.backend.auth.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; @@ -76,11 +77,12 @@ public void saveMemberWhenRequestIsValid() { when(memberRepository.save(any(Member.class))).thenReturn(member); // When - Member result = onboardingSignUpService.onboardingSignUp(member, request); + OnboardingSignUpResponse onboardingSignUpResponse = onboardingSignUpService.onboardingSignUp(member, request); // Then - assertNotNull(result); + assertNotNull(onboardingSignUpResponse); + verify(member).convertGuestToMember("조각조각", 2001, Gender.MALE, "http://test.jpg"); verify(memberRepository).save(member); } -} \ No newline at end of file +} From aa78a99f093e6f7232c0a470807ee3467bfa0012 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 24 Nov 2024 19:03:02 +0900 Subject: [PATCH 370/478] =?UTF-8?q?chore:=20(#86)=20HealthController?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ docker-compose.yml | 16 ++++++++++++++-- .../core/presentation/HealthController.java | 12 ++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/main/java/spring/backend/core/presentation/HealthController.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b368e9273..a90681ea5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -97,5 +97,7 @@ jobs: script: | export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} export GITHUB_SHA=${{ github.sha }} + export RABBITMQ_DEFAULT_USER=${{ secrets.RABBITMQ_DEFAULT_USER }} + export RABBITMQ_DEFAULT_PASS=${{ secrets.RABBITMQ_DEFAULT_PASS }} sudo chmod +x ./deploy.sh ./deploy.sh diff --git a/docker-compose.yml b/docker-compose.yml index 553d17770..dc8923188 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: - cnergy-backend: + blue: image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" - container_name: cnergy-backend + container_name: cnergy-backend-blue env_file: - .env ports: @@ -11,6 +11,18 @@ services: networks: - cnergy-backend-network + green: + image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + container_name: cnergy-backend-green + env_file: + - .env + ports: + - '8081:8080' + depends_on: + - redis + networks: + - cnergy-backend-network + redis: image: redis:6.0.9 container_name: redis diff --git a/src/main/java/spring/backend/core/presentation/HealthController.java b/src/main/java/spring/backend/core/presentation/HealthController.java new file mode 100644 index 000000000..031a854f6 --- /dev/null +++ b/src/main/java/spring/backend/core/presentation/HealthController.java @@ -0,0 +1,12 @@ +package spring.backend.core.presentation; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + @GetMapping("/health") + public String health() { + return "OK"; + } +} From b061d1690f4b1aa010c12a6a34c7d55497251594 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 25 Nov 2024 11:51:16 +0900 Subject: [PATCH 371/478] =?UTF-8?q?chore:=20(#86)=20PR=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=96=88?= =?UTF-8?q?=EC=9D=84=20=EA=B2=BD=EC=9A=B0,=20github.sha=EA=B0=80=20pr=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=9D=98=20=ED=95=B4=EC=8B=9C?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a90681ea5..51cdb4c92 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -81,7 +81,7 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.sha }} + tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-backend:${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} platforms: linux/amd64,linux/arm64 - name: Docker Compose 파일 NCP 서버로 전송 @@ -96,7 +96,7 @@ jobs: port: ${{ secrets.NCP_PORT }} script: | export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} - export GITHUB_SHA=${{ github.sha }} + export GITHUB_SHA=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} export RABBITMQ_DEFAULT_USER=${{ secrets.RABBITMQ_DEFAULT_USER }} export RABBITMQ_DEFAULT_PASS=${{ secrets.RABBITMQ_DEFAULT_PASS }} sudo chmod +x ./deploy.sh From 81fa56c05a425dbc4f392d7ce448e33a7af95072 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 25 Nov 2024 12:58:07 +0900 Subject: [PATCH 372/478] =?UTF-8?q?chore:=20(#86)=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EC=9D=98=20deploy.sha=20=EC=97=90=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=EB=A5=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 51cdb4c92..34ede148d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -95,9 +95,9 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | - export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} - export GITHUB_SHA=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - export RABBITMQ_DEFAULT_USER=${{ secrets.RABBITMQ_DEFAULT_USER }} - export RABBITMQ_DEFAULT_PASS=${{ secrets.RABBITMQ_DEFAULT_PASS }} + echo "NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }}" > .env + echo "GITHUB_SHA=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" >> .env + echo "RABBITMQ_DEFAULT_USER=${{ secrets.RABBITMQ_DEFAULT_USER }}" >> .env + echo "RABBITMQ_DEFAULT_PASS=${{ secrets.RABBITMQ_DEFAULT_PASS }}" >> .env sudo chmod +x ./deploy.sh ./deploy.sh From 23e490c8c07f16885886c7585d9381f02633303d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 25 Nov 2024 18:36:31 +0900 Subject: [PATCH 373/478] =?UTF-8?q?fix:=20(#139)=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20JVM=EC=9D=98=20=ED=83=80=EC=9E=84=EC=A1=B4?= =?UTF-8?q?=EC=9D=84=20Asia/Seoul=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index dc8923188..0afbdf0d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: container_name: cnergy-backend-blue env_file: - .env + environment: + TZ: Asia/Seoul ports: - '8080:8080' depends_on: @@ -16,6 +18,8 @@ services: container_name: cnergy-backend-green env_file: - .env + environment: + TZ: Asia/Seoul ports: - '8081:8080' depends_on: From 4edc19eb6ecc1ff5e737f413e11f2ab1db923c4c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 25 Nov 2024 23:35:51 +0900 Subject: [PATCH 374/478] =?UTF-8?q?fix:=20(#142)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=84=98=EA=B2=A8=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 2 +- .../auth/dto/response/LoginResponse.java | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 81305a7f6..160307ebe 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -53,6 +53,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - return LoginResponse.of(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member.getRole(), member.getCreatedAt()); + return LoginResponse.of(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member); } } diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java index 7f3d3c6c5..f165da425 100644 --- a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java +++ b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java @@ -1,6 +1,8 @@ package spring.backend.auth.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.value.Gender; import spring.backend.member.domain.value.Role; import java.time.LocalDateTime; @@ -13,13 +15,38 @@ public record LoginResponse( @Schema(description = "리프레시 토큰") String refreshToken, - @Schema(description = "사용자 유형(MEMBER, GUEST)", example = "MEMBER") - Role role, - - @Schema(description = "사용자 가입 날짜", example = "2024-11-14T06:10:55.091954") - LocalDateTime registrationDate + @Schema(description = "사용자 정보") + UserInfo userInfo ) { - public static LoginResponse of(String accessToken, String refreshToken, Role role, LocalDateTime registrationDate) { - return new LoginResponse(accessToken, refreshToken, role, registrationDate); + public record UserInfo( + + @Schema(description = "사용자 유형(MEMBER, GUEST)", example = "MEMBER") + Role role, + + @Schema(description = "사용자 이메일", example = "example@example.com") + String email, + + @Schema(description = "사용자 닉네임", example = "john_doe") + String nickname, + + @Schema(description = "사용자 출생 연도", example = "1990") + int birthYear, + + @Schema(description = "사용자 성별 (MALE, FEMALE, NONE)", example = "MALE") + Gender gender, + + @Schema(description = "사용자 프로필 이미지 URL", example = "https://example.com/profile.jpg") + String profileImage, + + @Schema(description = "사용자 가입 날짜", example = "2024-11-14T06:10:55.091954") + LocalDateTime registrationDate + ) { + public static UserInfo from(Member member) { + return new UserInfo(member.getRole(), member.getEmail(), member.getNickname(), member.getBirthYear(), member.getGender(), member.getProfileImage(), member.getCreatedAt()); + } + } + + public static LoginResponse of(String accessToken, String refreshToken, Member member) { + return new LoginResponse(accessToken, refreshToken, UserInfo.from(member)); } } From 41be3f22799ce6808ad2d656549c0dbf21519465 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 05:06:18 +0900 Subject: [PATCH 375/478] =?UTF-8?q?fix:=20(#146)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EC=98=A8=EB=B3=B4=EB=94=A9=EC=9D=84=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=98=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=9D=80=20null=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 1 - .../member/application/CreateMemberWithOAuthService.java | 4 ++-- src/main/java/spring/backend/member/domain/entity/Member.java | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 160307ebe..a2d7712dc 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -49,7 +49,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() .provider(provider) .email(oAuthResourceResponse.getEmail()) - .nickname(oAuthResourceResponse.getName()) .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java index 5e46a2310..957c6370a 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -24,7 +24,7 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { } List members = memberRepository.findAllByEmail(request.getEmail()); if (members == null || members.isEmpty()) { - Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); Member savedMember = memberRepository.save(newMember); if (savedMember == null) { @@ -49,7 +49,7 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { .filter(m -> m.isSameProvider(request.getProvider())) .findFirst() .orElseGet(() -> { - Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail(), request.getNickname()); + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); Member savedMember = memberRepository.save(newMember); if (savedMember == null) { diff --git a/src/main/java/spring/backend/member/domain/entity/Member.java b/src/main/java/spring/backend/member/domain/entity/Member.java index 392dc6d70..f8bae59ac 100644 --- a/src/main/java/spring/backend/member/domain/entity/Member.java +++ b/src/main/java/spring/backend/member/domain/entity/Member.java @@ -85,12 +85,11 @@ public void changeEmailNotification(boolean isEmailNotification) { this.emailNotification = isEmailNotification; } - public static Member createGuestMember(Provider provider, String email, String nickname) { + public static Member createGuestMember(Provider provider, String email) { return Member.builder() .provider(provider) .role(Role.GUEST) .email(email) - .nickname(nickname) .build(); } From 9ddd20aca0d590cd66f6d336c9812dad6449891a Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 05:37:20 +0900 Subject: [PATCH 376/478] =?UTF-8?q?feat:=20(#146)=20Role=EC=9D=B4=20GUEST?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EC=9D=80=20null=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMemberWithOAuthServiceTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java diff --git a/src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java b/src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java new file mode 100644 index 000000000..3fc099c3e --- /dev/null +++ b/src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java @@ -0,0 +1,50 @@ +package spring.backend.member.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Provider; +import spring.backend.member.domain.value.Role; +import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CreateMemberWithOAuthServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private CreateMemberWithOAuthService createMemberWithOAuthService; + + @DisplayName("Role이 GUEST인 경우 닉네임은 null이어야 한다") + @Test + void createGuestMember_returnsGuestMemberWithoutNickname() { + // given + CreateMemberWithOAuthRequest request = CreateMemberWithOAuthRequest.builder() + .provider(Provider.GOOGLE) + .email("jogakjogak@gmail.com") + .build(); + + // when + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); + when(memberRepository.save(any(Member.class))).thenReturn(newMember); + Member result = createMemberWithOAuthService.createMemberWithOAuth(request); + + // then + assertNotNull(result); + assertEquals(Provider.GOOGLE, result.getProvider()); + assertEquals("jogakjogak@gmail.com", result.getEmail()); + assertEquals(Role.GUEST, result.getRole()); + assertNull(result.getNickname()); + verify(memberRepository, times(1)).save(any(Member.class)); + } +} From 5771b2a80f779fa7fe43d6be3cd833d3bb44839e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 26 Nov 2024 02:06:48 +0900 Subject: [PATCH 377/478] =?UTF-8?q?feat:=20(#144)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 6 +++++ .../auth/presentation/LogoutController.java | 26 +++++++++++++++++++ .../presentation/swagger/LogoutSwagger.java | 21 +++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/main/java/spring/backend/auth/presentation/LogoutController.java create mode 100644 src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 765ecd48b..a286e9c7d 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -42,6 +42,12 @@ public String getRefreshToken(UUID memberId) { } public void deleteRefreshToken(UUID memberId) { + + if (refreshTokenRepository.findByMemberId(memberId) == null) { + log.error("memberId에 해당하는 리프레시 토큰이 저장소에 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); + } + refreshTokenRepository.deleteByMemberId(memberId); } diff --git a/src/main/java/spring/backend/auth/presentation/LogoutController.java b/src/main/java/spring/backend/auth/presentation/LogoutController.java new file mode 100644 index 000000000..05d99cd6e --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/LogoutController.java @@ -0,0 +1,26 @@ +package spring.backend.auth.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.auth.application.RefreshTokenService; +import spring.backend.auth.presentation.swagger.LogoutSwagger; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.member.domain.entity.Member; + +@RestController +@RequiredArgsConstructor +public class LogoutController implements LogoutSwagger { + + private final RefreshTokenService refreshTokenService; + + @PostMapping("/v1/logout") + @Authorization + @ResponseStatus(HttpStatus.OK) + public void logout(@AuthorizedMember Member member) { + refreshTokenService.deleteRefreshToken(member.getId()); + } +} diff --git a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java new file mode 100644 index 000000000..6abfe14cb --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java @@ -0,0 +1,21 @@ +package spring.backend.auth.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.member.domain.entity.Member; + +@Tag(name = "Auth", description = "인증/인가") +public interface LogoutSwagger { + + @Operation( + summary = "로그아웃 API", + description = "사용자의 로그아웃을 진행합니다. \n\n 로그아웃 시, 사용자의 토큰이 무효화되어, 다시 로그인을 진행해야 합니다.", + operationId = "/v1/logout" + ) + @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class}) + void logout(@Parameter(hidden = true) Member member); +} From 6dec3d8caf9245f05df640c8b09eb5cca5bc551e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 26 Nov 2024 02:10:04 +0900 Subject: [PATCH 378/478] =?UTF-8?q?feat:=20(#144)=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20Response=20d?= =?UTF-8?q?to=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MemberProfileResponse.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java diff --git a/src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java b/src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java new file mode 100644 index 000000000..aabe83431 --- /dev/null +++ b/src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java @@ -0,0 +1,17 @@ +package spring.backend.member.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.member.domain.entity.Member; + +public record MemberProfileResponse( + + @Schema(description = "이메일", example = "jogakjogak@gmail.com") + String email, + + @Schema(description = "이메일 알림 설정 여부", example = "true") + boolean isEmailNotificationEnabled +) { + public static MemberProfileResponse from(Member member) { + return new MemberProfileResponse(member.getEmail(), member.isEmailNotification()); + } +} From f179b4b7136712e5d0e61a2f8a5e6fb56ea4b4f1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 26 Nov 2024 02:10:15 +0900 Subject: [PATCH 379/478] =?UTF-8?q?feat:=20(#144)=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=99=80=20=EC=8A=A4=EC=9B=A8?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadMemberProfileController.java | 23 +++++++++++++++++++ .../swagger/ReadMemberProfileSwagger.java | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java create mode 100644 src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java diff --git a/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java b/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java new file mode 100644 index 000000000..37bd779a4 --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java @@ -0,0 +1,23 @@ +package spring.backend.member.presentation; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import spring.backend.core.configuration.argumentresolver.AuthorizedMember; +import spring.backend.core.configuration.interceptor.Authorization; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.MemberProfileResponse; +import spring.backend.member.presentation.swagger.ReadMemberProfileSwagger; + +@RestController +@RequestMapping +public class ReadMemberProfileController implements ReadMemberProfileSwagger { + + @Authorization + @GetMapping("/v1/profile") + public ResponseEntity> readMemberProfile(@AuthorizedMember Member member) { + return ResponseEntity.ok(new RestResponse<>(MemberProfileResponse.from(member))); + } +} diff --git a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java new file mode 100644 index 000000000..f6053792c --- /dev/null +++ b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java @@ -0,0 +1,23 @@ +package spring.backend.member.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.dto.response.MemberProfileResponse; + +@Tag(name = "Member", description = "멤버") +public interface ReadMemberProfileSwagger { + + @Operation( + summary = "프로필 조회 API", + description = "사용자의 프로필을 조회합니다.", + operationId = "/v1/profile" + ) + @ApiErrorCode({GlobalErrorCode.class}) + ResponseEntity> readMemberProfile(@Parameter(hidden = true) Member member); +} From c5ff56f919c42cef207ad6277b40f9f9e3355ad6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 26 Nov 2024 02:10:25 +0900 Subject: [PATCH 380/478] =?UTF-8?q?feat:=20(#144)=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/MemberProfileResponseTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java diff --git a/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java b/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java new file mode 100644 index 000000000..1addca58b --- /dev/null +++ b/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java @@ -0,0 +1,28 @@ +package spring.backend.member.dto.response; + + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import spring.backend.member.domain.entity.Member; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class MemberProfileResponseTest { + + @DisplayName("from 메서드가 올바르게 작동하는지 확인한다") + @Test + void testFromMethodInMemberProfileResponse() { + // given + Member member = Member.builder() + .email("jogakjogak@gmail.com") + .emailNotification(true) + .build(); + + // when + MemberProfileResponse response = MemberProfileResponse.from(member); + + // then + assertThat(response.email()).isEqualTo("jogakjogak@gmail.com"); + assertThat(response.isEmailNotificationEnabled()).isTrue(); + } +} From 0b5a0b26c492a73f32df1635edc8b31a36361933 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 06:05:45 +0900 Subject: [PATCH 381/478] =?UTF-8?q?refactor:=20(#148)=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20@Transactional=EB=A5=BC=20=EB=B6=99?= =?UTF-8?q?=EC=97=AC=20=EA=B0=80=EB=8F=85=EC=84=B1=EA=B3=BC=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=EC=84=B1=EC=9D=84=20=EB=86=92=EC=9D=BC=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMemberWithOAuthService.java | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java index 957c6370a..c07926a8e 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; @@ -13,6 +14,7 @@ @Service @RequiredArgsConstructor @Log4j2 +@Transactional public class CreateMemberWithOAuthService { private final MemberRepository memberRepository; @@ -22,22 +24,21 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { log.error("[createMemberWithOAuth] request is null"); throw MemberErrorCode.NOT_EXIST_CONDITION.toException(); } + List members = memberRepository.findAllByEmail(request.getEmail()); if (members == null || members.isEmpty()) { - Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); - Member savedMember = memberRepository.save(newMember); + return createGuestMember(request); + } - if (savedMember == null) { - log.error("[CreateMemberWithOAuthService] member could not be saved"); - throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); - } + return handleExistingMember(members, request); + } - return savedMember; - } + private Member handleExistingMember(List members, CreateMemberWithOAuthRequest request) { Member existingMember = members.stream() .filter(Member::isMember) .findFirst() .orElse(null); + if (existingMember != null) { if (!existingMember.isSameProvider(request.getProvider())) { log.error("[CreateMemberWithOAuthService] member already exists with a different provider [{}]", existingMember.getProvider()); @@ -45,19 +46,24 @@ public Member createMemberWithOAuth(CreateMemberWithOAuthRequest request) { } return existingMember; } + return members.stream() .filter(m -> m.isSameProvider(request.getProvider())) .findFirst() - .orElseGet(() -> { - Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); - Member savedMember = memberRepository.save(newMember); + .orElseGet(() -> createGuestMember(request)); + } - if (savedMember == null) { - log.error("[CreateMemberWithOAuthService] member could not be saved"); - throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); - } + private Member createGuestMember(CreateMemberWithOAuthRequest request) { + Member newMember = Member.createGuestMember(request.getProvider(), request.getEmail()); + return saveMember(newMember); + } - return savedMember; - }); + private Member saveMember(Member member) { + Member savedMember = memberRepository.save(member); + if (savedMember == null) { + log.error("[CreateMemberWithOAuthService] member could not be saved"); + throw MemberErrorCode.MEMBER_SAVE_FAILED.toException(); + } + return savedMember; } } From 692ce54033365c9d29e62fc6eb957ba760f24f1d Mon Sep 17 00:00:00 2001 From: anxi01 Date: Mon, 25 Nov 2024 12:25:00 +0900 Subject: [PATCH 382/478] =?UTF-8?q?fix:=20(#138)=20Keyword=20=EB=B0=B8?= =?UTF-8?q?=EB=A5=98=20=ED=83=80=EC=9E=85=20POJO=ED=99=94=20=EB=B0=8F=20@E?= =?UTF-8?q?mbeddable=EB=A1=9C=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/value/Keyword.java | 17 ++++++------- .../response/HomeActivityInfoResponse.java | 4 ++-- ...MonthlyActivityCountByKeywordResponse.java | 4 ++-- .../infrastructure/mapper/ActivityMapper.java | 16 ++++++++++--- .../jpa/entity/ActivityJpaEntity.java | 4 ++-- .../jpa/value/KeywordJpaValue.java | 24 +++++++++++++++++++ 6 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 src/main/java/spring/backend/activity/infrastructure/persistence/jpa/value/KeywordJpaValue.java diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 104e92564..4adb2ccb1 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -1,25 +1,22 @@ package spring.backend.activity.domain.value; -import jakarta.persistence.Embeddable; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import lombok.*; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; -@Embeddable @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode public class Keyword { - @Enumerated(EnumType.STRING) - private Category category; + private final Category category; - private String image; + private final String image; @Getter @RequiredArgsConstructor diff --git a/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java index 4c37a67d1..7e356239d 100644 --- a/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/HomeActivityInfoResponse.java @@ -1,7 +1,7 @@ package spring.backend.activity.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; public record HomeActivityInfoResponse( @@ -9,7 +9,7 @@ public record HomeActivityInfoResponse( Long id, @Schema(description = "활동 키워드", example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") - Keyword keyword, + KeywordJpaValue keyword, @Schema(description = "활동 제목", example = "마음의 편안을 가져다주는 명상음악 20분 듣기") String title, diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java index b2727e628..367b27f8c 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/MonthlyActivityCountByKeywordResponse.java @@ -1,11 +1,11 @@ package spring.backend.activity.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; public record MonthlyActivityCountByKeywordResponse( @Schema(description = "활동의 Keyword" , example = "{\"category\": \"SELF_DEVELOPMENT\", \"image\": \"https://example.com/image.jpg\"}") - Keyword keyword, + KeywordJpaValue keyword, @Schema(description = "Keyword별 활동 횟수" , example = "2") long activityCount diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java index b1caf605b..04df3dae6 100644 --- a/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java +++ b/src/main/java/spring/backend/activity/infrastructure/mapper/ActivityMapper.java @@ -2,7 +2,9 @@ import org.springframework.stereotype.Component; import spring.backend.activity.domain.entity.Activity; +import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; import java.util.Optional; @@ -16,7 +18,7 @@ public Activity toDomainEntity(ActivityJpaEntity activity) { .quickStartId(activity.getQuickStartId()) .spareTime(activity.getSpareTime()) .type(activity.getType()) - .keyword(activity.getKeyword()) + .keyword(toDomainValue(activity.getKeyword())) .title(activity.getTitle()) .content(activity.getContent()) .location(activity.getLocation()) @@ -36,7 +38,7 @@ public ActivityJpaEntity toJpaEntity(Activity activity) { .quickStartId(activity.getQuickStartId()) .spareTime(activity.getSpareTime()) .type(activity.getType()) - .keyword(activity.getKeyword()) + .keyword(toJpaValue(activity.getKeyword())) .title(activity.getTitle()) .content(activity.getContent()) .location(activity.getLocation()) @@ -48,4 +50,12 @@ public ActivityJpaEntity toJpaEntity(Activity activity) { .deleted(Optional.ofNullable(activity.getDeleted()).orElse(false)) .build(); } -} \ No newline at end of file + + private Keyword toDomainValue(KeywordJpaValue keywordJpaValue) { + return Keyword.create(keywordJpaValue.getCategory(), keywordJpaValue.getImage()); + } + + private KeywordJpaValue toJpaValue(Keyword keyword) { + return KeywordJpaValue.create(keyword.getCategory(), keyword.getImage()); + } +} diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java index 48ff05a78..65d5b3ad1 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/entity/ActivityJpaEntity.java @@ -5,7 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; import spring.backend.activity.domain.value.Type; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.infrastructure.jpa.shared.BaseLongIdEntity; @@ -30,7 +30,7 @@ public class ActivityJpaEntity extends BaseLongIdEntity { private Type type; @Embedded - private Keyword keyword; + private KeywordJpaValue keyword; private String title; diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/value/KeywordJpaValue.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/value/KeywordJpaValue.java new file mode 100644 index 000000000..8b20cd2ed --- /dev/null +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/value/KeywordJpaValue.java @@ -0,0 +1,24 @@ +package spring.backend.activity.infrastructure.persistence.jpa.value; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.*; +import spring.backend.activity.domain.value.Keyword.Category; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class KeywordJpaValue { + + @Enumerated(EnumType.STRING) + private Category category; + + private String image; + + public static KeywordJpaValue create(Category category, String image) { + return new KeywordJpaValue(category, image); + } +} From 7f782be4fc1bf72eb2ab87a71948e51d709d36df Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 27 Nov 2024 02:00:51 +0900 Subject: [PATCH 383/478] =?UTF-8?q?fix:=20(#153)=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=97=90=20=EA=B0=80=EC=9E=A5=20=EA=B0=80?= =?UTF-8?q?=EA=B9=8C=EC=9A=B4=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4=EC=A7=80=20=EB=AA=BB?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/dao/QuickStartJpaDao.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java index b481a6be4..93f29e158 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java @@ -37,8 +37,10 @@ public interface QuickStartJpaDao extends JpaRepository CURRENT_TIMESTAMP - order by q.startTime ASC + and (q.startTime > current_time or q.startTime <= current_time) + order by + case when q.startTime > current_time then 0 else 1 end, + q.startTime asc """) List findUpcomingQuickStarts(UUID memberId); -} \ No newline at end of file +} From c7bddff1270764b3297ba7aa7757b633792209fd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 26 Nov 2024 22:41:46 +0900 Subject: [PATCH 384/478] =?UTF-8?q?refactor:=20(#150)=20MemberID=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20Sequential=20UUID=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/jpa/shared/BaseEntity.java | 6 +- .../persistence/SequentialUUIDGenerator.java | 19 +++++ .../interceptor/SequentialUUID.java | 15 ++++ .../SequentialUUIDGeneratorTest.java | 80 +++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUIDGenerator.java create mode 100644 src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java create mode 100644 src/test/java/spring/backend/core/infrastructure/SequentialUUIDGeneratorTest.java diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java index 1cf61e76f..e37b1cf6c 100644 --- a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java @@ -3,9 +3,11 @@ import jakarta.persistence.*; import lombok.*; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.GenericGenerator; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import spring.backend.core.infrastructure.persistence.interceptor.SequentialUUID; import java.time.LocalDateTime; import java.util.UUID; @@ -18,7 +20,7 @@ public class BaseEntity { @Id - @GeneratedValue(strategy = GenerationType.UUID) + @SequentialUUID protected UUID id; @CreatedDate @@ -29,4 +31,4 @@ public class BaseEntity { @Builder.Default protected Boolean deleted = false; -} \ No newline at end of file +} diff --git a/src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUIDGenerator.java b/src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUIDGenerator.java new file mode 100644 index 000000000..82d1bfe15 --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUIDGenerator.java @@ -0,0 +1,19 @@ +package spring.backend.core.infrastructure.persistence; + +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.id.IdentifierGenerator; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +public class SequentialUUIDGenerator implements IdentifierGenerator { + + @Override + public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException { + long mostSignificantBits = Instant.now().toEpochMilli(); + long leastSignificantBits = UUID.randomUUID().getLeastSignificantBits(); + return new UUID(mostSignificantBits, leastSignificantBits); + } +} diff --git a/src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java b/src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java new file mode 100644 index 000000000..b1670ae2b --- /dev/null +++ b/src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java @@ -0,0 +1,15 @@ +package spring.backend.core.infrastructure.persistence.interceptor; + +import org.hibernate.annotations.IdGeneratorType; +import spring.backend.core.infrastructure.persistence.SequentialUUIDGenerator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@IdGeneratorType(SequentialUUIDGenerator.class) +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SequentialUUID { +} diff --git a/src/test/java/spring/backend/core/infrastructure/SequentialUUIDGeneratorTest.java b/src/test/java/spring/backend/core/infrastructure/SequentialUUIDGeneratorTest.java new file mode 100644 index 000000000..aa83e2fbc --- /dev/null +++ b/src/test/java/spring/backend/core/infrastructure/SequentialUUIDGeneratorTest.java @@ -0,0 +1,80 @@ +package spring.backend.core.infrastructure; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.repository.MemberRepository; +import spring.backend.member.domain.value.Provider; +import spring.backend.member.domain.value.Role; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +@SpringBootTest +public class SequentialUUIDGeneratorTest { + + @Autowired + private MemberRepository memberRepository; + + @DisplayName("UUID가 순차적으로 생성되는지 확인한다.") + @Test + void testSequentialUUIDGenerator() { + // given + Member member1 = Member.builder() + .provider(Provider.GOOGLE) + .role(Role.GUEST) + .email("test1@test.com") + .build(); + + Member member2 = Member.builder() + .provider(Provider.GOOGLE) + .role(Role.GUEST) + .email("test2@test.com") + .build(); + + // when + Member saverMember1 = memberRepository.save(member1); + Member savedMember2 = memberRepository.save(member2); + + // then + assertNotNull(saverMember1.getId()); + assertNotNull(savedMember2.getId()); + + long timestamp1 = saverMember1.getId().getMostSignificantBits() >>> 16; + long timestamp2 = savedMember2.getId().getMostSignificantBits() >>> 16; + assertTrue(timestamp2 >= timestamp1); + } + + @DisplayName("생성순서를 변경할 경우, UUID가 순차적으로 생성되는지 확인한다.") + @Test + void failedTestSequentialUUIDGenerator() { + // given + Member member1 = Member.builder() + .provider(Provider.GOOGLE) + .role(Role.GUEST) + .email("test1@test.com") + .build(); + + Member member2 = Member.builder() + .provider(Provider.GOOGLE) + .role(Role.GUEST) + .email("test2@test.com") + .build(); + + // when + Member savedMember2 = memberRepository.save(member2); + Member saverMember1 = memberRepository.save(member1); + + // then + assertNotNull(saverMember1.getId()); + assertNotNull(savedMember2.getId()); + + long timestamp1 = saverMember1.getId().getMostSignificantBits() >>> 16; + long timestamp2 = savedMember2.getId().getMostSignificantBits() >>> 16; + + assertTrue(timestamp2 <= timestamp1); + } +} From 38d12a9746a58e22202d42fd30c0051ac32de639 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 27 Nov 2024 15:00:57 +0900 Subject: [PATCH 385/478] =?UTF-8?q?refactor:=20(#150)=20SequentialUUID?= =?UTF-8?q?=EC=9D=98=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/infrastructure/jpa/shared/BaseEntity.java | 3 +-- .../persistence/{interceptor => }/SequentialUUID.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) rename src/main/java/spring/backend/core/infrastructure/persistence/{interceptor => }/SequentialUUID.java (63%) diff --git a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java index e37b1cf6c..753fed82f 100644 --- a/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java +++ b/src/main/java/spring/backend/core/infrastructure/jpa/shared/BaseEntity.java @@ -3,11 +3,10 @@ import jakarta.persistence.*; import lombok.*; import lombok.experimental.SuperBuilder; -import org.hibernate.annotations.GenericGenerator; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import spring.backend.core.infrastructure.persistence.interceptor.SequentialUUID; +import spring.backend.core.infrastructure.persistence.SequentialUUID; import java.time.LocalDateTime; import java.util.UUID; diff --git a/src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java b/src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUID.java similarity index 63% rename from src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java rename to src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUID.java index b1670ae2b..e56674f1e 100644 --- a/src/main/java/spring/backend/core/infrastructure/persistence/interceptor/SequentialUUID.java +++ b/src/main/java/spring/backend/core/infrastructure/persistence/SequentialUUID.java @@ -1,7 +1,6 @@ -package spring.backend.core.infrastructure.persistence.interceptor; +package spring.backend.core.infrastructure.persistence; import org.hibernate.annotations.IdGeneratorType; -import spring.backend.core.infrastructure.persistence.SequentialUUIDGenerator; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,7 +8,7 @@ import java.lang.annotation.Target; @IdGeneratorType(SequentialUUIDGenerator.class) -@Target({ElementType.METHOD, ElementType.FIELD}) +@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface SequentialUUID { } From e650afb39964b31b07fa11adfd19f2551d2ece05 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 02:56:55 +0900 Subject: [PATCH 386/478] =?UTF-8?q?fix:=20(#157)=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=EC=97=90=EC=84=9C=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=ED=88=AC=EB=AA=85=EB=8F=84=2030=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...vitiesByMemberAndKeywordInMonthService.java | 2 +- .../configuration/property/ImageProperty.java | 7 +++++++ .../backend/core/converter/ImageConverter.java | 18 ++++++++++++++++++ .../core/converter/ImageConverterTest.java | 13 +++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index a1049f43a..cc1f73625 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -29,7 +29,7 @@ public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeyw LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); TotalSavedTimeAndActivityCountByKeywordInMonth totalSavedTimeAndActivityCountByKeywordInMonth = activityDao.findTotalSavedTimeAndActivityCountByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - Keyword keyword = Keyword.create(keywordCategory, imageConverter.convertToImageUrl(keywordCategory)); + Keyword keyword = Keyword.create(keywordCategory, imageConverter.convertToTransparent30ImageUrl(keywordCategory)); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeAndActivityCountByKeywordInMonth.totalSavedTimeByKeywordInMonth(), totalSavedTimeAndActivityCountByKeywordInMonth.totalActivityCountByKeywordInMonth(), diff --git a/src/main/java/spring/backend/core/configuration/property/ImageProperty.java b/src/main/java/spring/backend/core/configuration/property/ImageProperty.java index ae66ccccf..3b679c6f5 100644 --- a/src/main/java/spring/backend/core/configuration/property/ImageProperty.java +++ b/src/main/java/spring/backend/core/configuration/property/ImageProperty.java @@ -17,4 +17,11 @@ public class ImageProperty { private String entertainmentImageUrl; private String relaxationImageUrl; private String socialImageUrl; + + private String transparent30SelfDevelopmentImageUrl; + private String transparent30HealthImageUrl; + private String transparent30CultureArtImageUrl; + private String transparent30EntertainmentImageUrl; + private String transparent30RelaxationImageUrl; + private String transparent30SocialImageUrl; } diff --git a/src/main/java/spring/backend/core/converter/ImageConverter.java b/src/main/java/spring/backend/core/converter/ImageConverter.java index c935bfd35..cf8a8be29 100644 --- a/src/main/java/spring/backend/core/converter/ImageConverter.java +++ b/src/main/java/spring/backend/core/converter/ImageConverter.java @@ -15,6 +15,7 @@ public class ImageConverter { private final ImageProperty imageProperty; private final Map categoryImageMap = new HashMap<>(); + private final Map categoryTransparent30ImageMap = new HashMap<>(); @PostConstruct private void initializeImageMap() { @@ -26,10 +27,27 @@ private void initializeImageMap() { categoryImageMap.put(Category.SOCIAL, imageProperty.getSocialImageUrl()); } + @PostConstruct + private void initializeTransparent30ImageMap() { + categoryTransparent30ImageMap.put(Category.SELF_DEVELOPMENT, imageProperty.getTransparent30SelfDevelopmentImageUrl()); + categoryTransparent30ImageMap.put(Category.HEALTH, imageProperty.getTransparent30HealthImageUrl()); + categoryTransparent30ImageMap.put(Category.CULTURE_ART, imageProperty.getTransparent30CultureArtImageUrl()); + categoryTransparent30ImageMap.put(Category.ENTERTAINMENT, imageProperty.getTransparent30EntertainmentImageUrl()); + categoryTransparent30ImageMap.put(Category.RELAXATION, imageProperty.getTransparent30RelaxationImageUrl()); + categoryTransparent30ImageMap.put(Category.SOCIAL, imageProperty.getTransparent30SocialImageUrl()); + } + public String convertToImageUrl(Category category) { if (category == null) { return null; } return categoryImageMap.get(category); } + + public String convertToTransparent30ImageUrl(Category category) { + if (category == null) { + return null; + } + return categoryTransparent30ImageMap.get(category); + } } diff --git a/src/test/java/spring/backend/core/converter/ImageConverterTest.java b/src/test/java/spring/backend/core/converter/ImageConverterTest.java index 0f5ffbd0a..f3f3e70f2 100644 --- a/src/test/java/spring/backend/core/converter/ImageConverterTest.java +++ b/src/test/java/spring/backend/core/converter/ImageConverterTest.java @@ -31,4 +31,17 @@ void mapCategoryToImageUrl() { assertNull(imageConverter.convertToImageUrl(null)); } + + @DisplayName("주어진 카테고리에 맞는 투명도 30% 이미지 URL을 반환한다") + @Test + void mapCategoryTransparent30ImageUrl() { + assertEquals(imageProperty.getTransparent30SelfDevelopmentImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.SELF_DEVELOPMENT)); + assertEquals(imageProperty.getTransparent30HealthImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.HEALTH)); + assertEquals(imageProperty.getTransparent30CultureArtImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.CULTURE_ART)); + assertEquals(imageProperty.getTransparent30EntertainmentImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.ENTERTAINMENT)); + assertEquals(imageProperty.getTransparent30RelaxationImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.RELAXATION)); + assertEquals(imageProperty.getTransparent30SocialImageUrl(), imageConverter.convertToTransparent30ImageUrl(Category.SOCIAL)); + + assertNull(imageConverter.convertToTransparent30ImageUrl(null)); + } } From a540e3a207b33080f5ca20af2fcd5f5d43e8e7cc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 22:50:29 +0900 Subject: [PATCH 387/478] =?UTF-8?q?feat:=20(#151)=20Thymeleaf=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index d2a606fc7..daf4788be 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,9 @@ dependencies { // Spring Batch implementation 'org.springframework.boot:spring-boot-starter-batch' testImplementation 'org.springframework.batch:spring-batch-test' + + // Thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' } tasks.named('test') { From e61b2678b6f44db8eb53200d2e77a52fcacfcb27 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 22:52:42 +0900 Subject: [PATCH 388/478] =?UTF-8?q?fix:=20(#151)=20=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=EC=8B=9C=20HTML=EC=9D=84=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EB=84=A3=EA=B8=B0=EC=9C=84=ED=95=B4=20Mim?= =?UTF-8?q?eMessage=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/util/email/EmailUtil.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index 3fe784cd7..907f360ed 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -1,10 +1,16 @@ package spring.backend.core.util.email; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.*; +import org.springframework.mail.MailAuthenticationException; +import org.springframework.mail.MailException; +import org.springframework.mail.MailParseException; +import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.core.util.email.exception.MailErrorCode; @@ -28,11 +34,14 @@ public class EmailUtil { public void send(SendEmailRequest sendEmailRequest) { validateEmailRequest(sendEmailRequest); try { - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(sender); - message.setTo(sendEmailRequest.receivers()); - message.setSubject(sendEmailRequest.title()); - message.setText(sendEmailRequest.content()); + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(sender); + helper.setTo(sendEmailRequest.receivers()); + helper.setSubject(sendEmailRequest.title()); + helper.setText(sendEmailRequest.content(), true); + mailSender.send(message); } catch (MailParseException e) { log.error("[EmailUtil] Failed to parse email for recipient: {}, subject: {}. Error: {}", @@ -46,7 +55,7 @@ public void send(SendEmailRequest sendEmailRequest) { log.error("[EmailUtil] Error occurred while sending email to recipient: {}, subject: {}. Error: {}", Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.ERROR_OCCURRED_SENDING_MAIL.toException(); - } catch (MailException e) { + } catch (MailException | MessagingException e) { log.error("[EmailUtil] General mail error for recipient: {}, subject: {}. Error: {}", Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.GENERAL_MAIL_ERROR.toException(); From e31c6b3b3083041177b77b4288e381d3f90172e5 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Tue, 26 Nov 2024 22:54:03 +0900 Subject: [PATCH 389/478] =?UTF-8?q?feat:=20(#151)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=EC=9D=84=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SendQuickStartEmailsScheduler.java | 11 +++++++++- src/main/resources/templates/mail.html | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/templates/mail.html diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java index 46fcb55a6..eaf0439ba 100644 --- a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java +++ b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java @@ -4,6 +4,8 @@ import lombok.extern.log4j.Log4j2; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; import spring.backend.activity.domain.entity.QuickStart; import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.core.util.email.EmailUtil; @@ -27,6 +29,8 @@ public class SendQuickStartEmailsScheduler { private final EmailUtil emailUtil; + private final TemplateEngine templateEngine; + @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmails() { List receivers = new ArrayList<>(); @@ -51,7 +55,7 @@ public void sendQuickStartEmails() { if (!receivers.isEmpty()) { SendEmailRequest request = SendEmailRequest.builder() .title("Test Title") - .content("Test Content") + .content(generateEmailContent()) .receivers(receivers.toArray(new String[0])) .build(); @@ -65,4 +69,9 @@ public void sendQuickStartEmails() { log.warn("[SendQuickStartEmailsScheduler] No valid receivers found in the time range: {} - {}", lowerBound, upperBound); } } + + private String generateEmailContent() { + Context context = new Context(); + return templateEngine.process("mail", context); + } } diff --git a/src/main/resources/templates/mail.html b/src/main/resources/templates/mail.html new file mode 100644 index 000000000..e2c8cae3a --- /dev/null +++ b/src/main/resources/templates/mail.html @@ -0,0 +1,22 @@ + + + + + 조각조각 메일 + + + + + + +
+ + 조각조각 이미지 + +
+ + From 215d6fabac04022b43a8c1eb3ca8ff24f11db4fc Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 27 Nov 2024 21:50:48 +0900 Subject: [PATCH 390/478] =?UTF-8?q?refactor:=20(#151)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=8F=20=EB=B0=B0=EC=B9=98=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=B6=84=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SendQuickStartEmailsScheduler.java | 30 ++++++--- .../batch/job/SendQuickStartEmailsJob.java | 67 ++++++++++++------- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java index eaf0439ba..ab7b6d0ac 100644 --- a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java +++ b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java @@ -14,9 +14,7 @@ import spring.backend.member.domain.repository.MemberRepository; import java.time.LocalTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; @Service @RequiredArgsConstructor @@ -33,8 +31,17 @@ public class SendQuickStartEmailsScheduler { @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmails() { - List receivers = new ArrayList<>(); + Set receivers = collectEmailReceiversWithinTimeRange(); + if (!receivers.isEmpty()) { + sendEmails(receivers); + } else { + log.warn("[SendQuickStartEmailsScheduler] No valid receiver found in the time range."); + } + } + + private Set collectEmailReceiversWithinTimeRange() { + Set receivers = new HashSet<>(); final int TIME_INTERVAL_MINUTES = 15; LocalTime now = LocalTime.now(); LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); @@ -52,21 +59,22 @@ public void sendQuickStartEmails() { .filter(Objects::nonNull) .forEach(receivers::add); - if (!receivers.isEmpty()) { + return receivers; + } + + private void sendEmails(Set receivers) { + for (String receiver : receivers) { SendEmailRequest request = SendEmailRequest.builder() .title("Test Title") .content(generateEmailContent()) - .receivers(receivers.toArray(new String[0])) + .receiver(receiver) .build(); - try { emailUtil.send(request); - log.info("[SendQuickStartEmailsScheduler] Successfully sent email to {} receivers", receivers.size()); + log.info("[SendQuickStartEmailsScheduler] Successfully sent email to {}", receiver); } catch (Exception e) { - log.error("[SendQuickStartEmailsScheduler] Failed to send email to {} receivers", receivers.size(), e); + log.error("[SendQuickStartEmailsScheduler] Failed to send email to {}", receiver, e); } - } else { - log.warn("[SendQuickStartEmailsScheduler] No valid receivers found in the time range: {} - {}", lowerBound, upperBound); } } diff --git a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java index 2090c6948..4f05a127f 100644 --- a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java +++ b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java @@ -14,6 +14,8 @@ import org.springframework.batch.repeat.RepeatStatus; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; import spring.backend.core.util.email.EmailUtil; @@ -23,10 +25,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; @Configuration @RequiredArgsConstructor @@ -45,6 +44,8 @@ public class SendQuickStartEmailsJob { private final EmailUtil emailUtil; + private final TemplateEngine templateEngine; + // @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmailsJobScheduler() { try { @@ -73,7 +74,7 @@ public Step sendQuickStartEmailsStep() { public Tasklet sendQuickStartEmailsTasklet() { return (contribution, chunkContext) -> { - List receivers = new ArrayList<>(); + Set receivers; try { final int TIME_INTERVAL_MINUTES = 15; LocalTime now = LocalTime.now(); @@ -84,29 +85,12 @@ public Tasklet sendQuickStartEmailsTasklet() { List quickStarts = quickStartJpaRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); - quickStarts.stream() - .map(QuickStartJpaEntity::getMemberId) - .map(memberJpaRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .map(MemberJpaEntity::getEmail) - .forEach(receivers::add); + receivers = collectReceiversFromQuickStarts(quickStarts); if (!receivers.isEmpty()) { - SendEmailRequest request = SendEmailRequest.builder() - .title("Test Title") - .content("Test Content") - .receivers(receivers.toArray(new String[0])) - .build(); - - try { - emailUtil.send(request); - log.info("[SendQuickStartEmailsJob] Successfully sent email to {} receivers", receivers.size()); - } catch (Exception e) { - log.error("[SendQuickStartEmailsJob] Failed to send email to {} receivers", receivers.size(), e); - } + sendEmailsToReceivers(receivers); } else { - log.warn("[SendQuickStartEmailsJob] No valid receivers found in the time range: {} - {}", lowerBound, upperBound); + log.warn("[SendQuickStartEmailsJob] No valid receiver found in the time range: {} - {}", lowerBound, upperBound); } } catch (Exception e) { log.error("[SendQuickStartEmailsJob] Error during tasklet execution", e); @@ -114,4 +98,37 @@ public Tasklet sendQuickStartEmailsTasklet() { return RepeatStatus.FINISHED; }; } + + private Set collectReceiversFromQuickStarts(List quickStarts) { + Set receivers = new HashSet<>(); + quickStarts.stream() + .map(QuickStartJpaEntity::getMemberId) + .map(memberJpaRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .map(MemberJpaEntity::getEmail) + .forEach(receivers::add); + return receivers; + } + + private void sendEmailsToReceivers(Set receivers) { + for (String receiver : receivers) { + SendEmailRequest request = SendEmailRequest.builder() + .title("Test Title") + .content(generateEmailContent()) + .receiver(receiver) + .build(); + try { + emailUtil.send(request); + log.info("[SendQuickStartEmailsJob] Successfully sent email to {}", receiver); + } catch (Exception e) { + log.error("[SendQuickStartEmailsJob] Failed to send email to {}", receiver, e); + } + } + } + + private String generateEmailContent() { + Context context = new Context(); + return templateEngine.process("mail", context); + } } From 6a8d84ece962d1a9771029a5356cf594b653b511 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 27 Nov 2024 21:51:41 +0900 Subject: [PATCH 391/478] =?UTF-8?q?fix:=20(#151)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=8B=A0=EC=9E=90=EB=A5=BC=20=EB=B2=8C?= =?UTF-8?q?=ED=81=AC=20=EB=8B=A8=EC=9C=84=EC=97=90=EC=84=9C=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/util/email/EmailUtil.java | 13 ++++++------- .../util/email/dto/request/SendEmailRequest.java | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index 907f360ed..dea46e7d3 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -15,7 +15,6 @@ import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.core.util.email.exception.MailErrorCode; -import java.util.Arrays; import java.util.regex.Pattern; @Component @@ -38,14 +37,14 @@ public void send(SendEmailRequest sendEmailRequest) { MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setFrom(sender); - helper.setTo(sendEmailRequest.receivers()); + helper.setTo(sendEmailRequest.receiver()); helper.setSubject(sendEmailRequest.title()); helper.setText(sendEmailRequest.content(), true); mailSender.send(message); } catch (MailParseException e) { log.error("[EmailUtil] Failed to parse email for recipient: {}, subject: {}. Error: {}", - Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.FAILED_TO_PARSE_MAIL.toException(); } catch (MailAuthenticationException e) { log.error("[EmailUtil] Authentication failed for email sender: {}. Error: {}", @@ -53,11 +52,11 @@ public void send(SendEmailRequest sendEmailRequest) { throw MailErrorCode.AUTHENTICATION_FAILED.toException(); } catch (MailSendException e) { log.error("[EmailUtil] Error occurred while sending email to recipient: {}, subject: {}. Error: {}", - Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.ERROR_OCCURRED_SENDING_MAIL.toException(); } catch (MailException | MessagingException e) { log.error("[EmailUtil] General mail error for recipient: {}, subject: {}. Error: {}", - Arrays.toString(sendEmailRequest.receivers()), sendEmailRequest.title(), e.getMessage()); + sendEmailRequest.receiver(), sendEmailRequest.title(), e.getMessage()); throw MailErrorCode.GENERAL_MAIL_ERROR.toException(); } } @@ -69,8 +68,8 @@ private void validateEmailRequest(SendEmailRequest request) { } private void validateEmailAddress(SendEmailRequest request) { - if (request.receivers() == null || Arrays.stream(request.receivers()).anyMatch(email -> !EMAIL_PATTERN.matcher(email).matches())) { - log.error("[EmailUtil] Invalid email address format: {}", Arrays.toString(request.receivers())); + if (request.receiver() == null || !EMAIL_PATTERN.matcher(request.receiver()).matches()) { + log.error("[EmailUtil] Invalid email address format: {}", request.receiver()); throw MailErrorCode.INVALID_MAIL_ADDRESS.toException(); } } diff --git a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java index 16b0d558c..a60e56412 100644 --- a/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java +++ b/src/main/java/spring/backend/core/util/email/dto/request/SendEmailRequest.java @@ -4,7 +4,7 @@ @Builder public record SendEmailRequest( - String[] receivers, + String receiver, String title, String content ) { From b1a98e79d01f0a9c18ae859a3163247cfb14126c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 27 Nov 2024 21:52:36 +0900 Subject: [PATCH 392/478] =?UTF-8?q?fix:=20(#151)=20=EB=B9=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=EC=9D=98=20=EC=8B=9C=EA=B0=84=EB=8C=80?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=ED=95=98=EB=8A=94=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A4=91=EB=B3=B5=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/repository/QuickStartJpaRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java index 955132c82..0b1fcf677 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java @@ -12,11 +12,11 @@ public interface QuickStartJpaRepository extends JpaRepository Date: Wed, 27 Nov 2024 21:53:30 +0900 Subject: [PATCH 393/478] =?UTF-8?q?feat:=20(#151)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EB=B9=84=EB=8F=99=EA=B8=B0=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=84=EC=86=A1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/AsyncConfiguration.java | 24 +++++++++++++++++++ .../backend/core/util/email/EmailUtil.java | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/AsyncConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/AsyncConfiguration.java b/src/main/java/spring/backend/core/configuration/AsyncConfiguration.java new file mode 100644 index 000000000..aed540bb8 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/AsyncConfiguration.java @@ -0,0 +1,24 @@ +package spring.backend.core.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AsyncConfiguration { + + @Bean + public Executor mailExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(30); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("MailExecutor-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/spring/backend/core/util/email/EmailUtil.java b/src/main/java/spring/backend/core/util/email/EmailUtil.java index dea46e7d3..60366a808 100644 --- a/src/main/java/spring/backend/core/util/email/EmailUtil.java +++ b/src/main/java/spring/backend/core/util/email/EmailUtil.java @@ -11,6 +11,7 @@ import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.core.util.email.exception.MailErrorCode; @@ -30,6 +31,7 @@ public class EmailUtil { private final JavaMailSender mailSender; + @Async("mailExecutor") public void send(SendEmailRequest sendEmailRequest) { validateEmailRequest(sendEmailRequest); try { From 352d9955eccbe20aefc60914ff8c6e099a921741 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Wed, 27 Nov 2024 22:26:25 +0900 Subject: [PATCH 394/478] =?UTF-8?q?fix:=20(#151)=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=8B=A0=EC=9E=90=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20=EB=A9=A4=EB=B2=84=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SendQuickStartEmailsScheduler.java | 31 ++++--------- .../repository/QuickStartRepository.java | 4 -- .../batch/job/SendQuickStartEmailsJob.java | 43 ++++++------------- .../jpa/adapter/QuickStartRepositoryImpl.java | 16 ------- .../repository/QuickStartJpaRepository.java | 17 -------- .../domain/repository/MemberRepository.java | 2 + .../jpa/adapter/MemberRepositoryImpl.java | 10 +++++ .../jpa/repository/MemberJpaRepository.java | 11 +++++ 8 files changed, 45 insertions(+), 89 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java index ab7b6d0ac..2a702fcab 100644 --- a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java +++ b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java @@ -6,23 +6,19 @@ import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; import java.time.LocalTime; -import java.util.*; +import java.util.List; @Service @RequiredArgsConstructor @Log4j2 public class SendQuickStartEmailsScheduler { - private final QuickStartRepository quickStartRepository; - private final MemberRepository memberRepository; private final EmailUtil emailUtil; @@ -31,7 +27,7 @@ public class SendQuickStartEmailsScheduler { @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmails() { - Set receivers = collectEmailReceiversWithinTimeRange(); + List receivers = collectEmailReceiversWithinTimeRange(); if (!receivers.isEmpty()) { sendEmails(receivers); @@ -40,34 +36,23 @@ public void sendQuickStartEmails() { } } - private Set collectEmailReceiversWithinTimeRange() { - Set receivers = new HashSet<>(); + private List collectEmailReceiversWithinTimeRange() { final int TIME_INTERVAL_MINUTES = 15; LocalTime now = LocalTime.now(); LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); - log.info("[SendQuickStartEmailsScheduler] Searching for QuickStarts between {} and {}", lowerBound, upperBound); - - List quickStarts = quickStartRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); - - quickStarts.stream() - .map(QuickStart::getMemberId) - .map(memberRepository::findById) - .filter(Objects::nonNull) - .map(Member::getEmail) - .filter(Objects::nonNull) - .forEach(receivers::add); + log.info("[SendQuickStartEmailsScheduler] Searching for receivers between {} and {}", lowerBound, upperBound); - return receivers; + return memberRepository.findMembersForQuickStartsInTimeRange(lowerBound, upperBound); } - private void sendEmails(Set receivers) { - for (String receiver : receivers) { + private void sendEmails(List receivers) { + for (Member receiver : receivers) { SendEmailRequest request = SendEmailRequest.builder() .title("Test Title") .content(generateEmailContent()) - .receiver(receiver) + .receiver(receiver.getEmail()) .build(); try { emailUtil.send(request); diff --git a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java index 377f2996b..5e483a366 100644 --- a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java +++ b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java @@ -2,12 +2,8 @@ import spring.backend.activity.domain.entity.QuickStart; -import java.time.LocalTime; -import java.util.List; - public interface QuickStartRepository { QuickStart findById(Long id); QuickStart save(QuickStart quickStart); - List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound); } diff --git a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java index 4f05a127f..bca11f629 100644 --- a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java +++ b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java @@ -16,8 +16,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; -import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; @@ -38,8 +36,6 @@ public class SendQuickStartEmailsJob { private final PlatformTransactionManager platformTransactionManager; - private final QuickStartJpaRepository quickStartJpaRepository; - private final MemberJpaRepository memberJpaRepository; private final EmailUtil emailUtil; @@ -74,23 +70,13 @@ public Step sendQuickStartEmailsStep() { public Tasklet sendQuickStartEmailsTasklet() { return (contribution, chunkContext) -> { - Set receivers; try { - final int TIME_INTERVAL_MINUTES = 15; - LocalTime now = LocalTime.now(); - LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); - LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); - - log.info("[SendQuickStartEmailsJob] Searching for QuickStarts between {} and {}", lowerBound, upperBound); - - List quickStarts = quickStartJpaRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); - - receivers = collectReceiversFromQuickStarts(quickStarts); + List receivers = collectEmailReceiversWithinTimeRange(); if (!receivers.isEmpty()) { sendEmailsToReceivers(receivers); } else { - log.warn("[SendQuickStartEmailsJob] No valid receiver found in the time range: {} - {}", lowerBound, upperBound); + log.warn("[SendQuickStartEmailsJob] No valid receiver found in the time range."); } } catch (Exception e) { log.error("[SendQuickStartEmailsJob] Error during tasklet execution", e); @@ -99,24 +85,23 @@ public Tasklet sendQuickStartEmailsTasklet() { }; } - private Set collectReceiversFromQuickStarts(List quickStarts) { - Set receivers = new HashSet<>(); - quickStarts.stream() - .map(QuickStartJpaEntity::getMemberId) - .map(memberJpaRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .map(MemberJpaEntity::getEmail) - .forEach(receivers::add); - return receivers; + private List collectEmailReceiversWithinTimeRange() { + final int TIME_INTERVAL_MINUTES = 15; + LocalTime now = LocalTime.now(); + LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); + LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + + log.info("[SendQuickStartEmailsJob] Searching for receivers between {} and {}", lowerBound, upperBound); + + return memberJpaRepository.findMembersForQuickStartsInTimeRange(lowerBound, upperBound); } - private void sendEmailsToReceivers(Set receivers) { - for (String receiver : receivers) { + private void sendEmailsToReceivers(List receivers) { + for (MemberJpaEntity receiver : receivers) { SendEmailRequest request = SendEmailRequest.builder() .title("Test Title") .content(generateEmailContent()) - .receiver(receiver) + .receiver(receiver.getEmail()) .build(); try { emailUtil.send(request); diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java index f7dd091d7..1545e6f62 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java @@ -10,12 +10,6 @@ import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; -import java.time.LocalTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - @Repository @RequiredArgsConstructor @Log4j2 @@ -44,14 +38,4 @@ public QuickStart save(QuickStart quickStart) { throw QuickStartErrorCode.QUICK_START_SAVE_FAILED.toException(); } } - - @Override - public List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound) { - List quickStartJpaEntities = quickStartJpaRepository.findQuickStartsWithinTimeRange(lowerBound, upperBound); - return Optional.ofNullable(quickStartJpaEntities) - .orElse(Collections.emptyList()) - .stream() - .map(quickStartMapper::toDomainEntity) - .collect(Collectors.toList()); - } } diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java index 0b1fcf677..548dafd6c 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java @@ -1,24 +1,7 @@ package spring.backend.activity.infrastructure.persistence.jpa.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; -import java.time.LocalTime; -import java.util.List; - public interface QuickStartJpaRepository extends JpaRepository { - - @Query(""" - select q - from QuickStartJpaEntity q - where q.startTime = ( - select MIN(q2.startTime) - from QuickStartJpaEntity q2 - where q2.memberId = q.memberId - and q2.startTime between :lowerBound and :upperBound - group by q2.memberId - ) - """) - List findQuickStartsWithinTimeRange(LocalTime lowerBound, LocalTime upperBound); } diff --git a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java index 46dc09749..b4be90939 100644 --- a/src/main/java/spring/backend/member/domain/repository/MemberRepository.java +++ b/src/main/java/spring/backend/member/domain/repository/MemberRepository.java @@ -3,6 +3,7 @@ import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; +import java.time.LocalTime; import java.util.List; import java.util.UUID; @@ -13,4 +14,5 @@ public interface MemberRepository { Member findByEmail(String email); List findAllByEmail(String email); boolean existsByNicknameAndRole(String nickname, Role role); + List findMembersForQuickStartsInTimeRange(LocalTime lowerBound, LocalTime upperBound); } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java index 729bc7054..449aac282 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/adapter/MemberRepositoryImpl.java @@ -11,6 +11,7 @@ import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; import spring.backend.member.infrastructure.persistence.jpa.repository.MemberJpaRepository; +import java.time.LocalTime; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -64,4 +65,13 @@ public List findAllByEmail(String email) { public boolean existsByNicknameAndRole(String nickname, Role role) { return memberJpaRepository.existsByNicknameAndRole(nickname, role); } + + @Override + public List findMembersForQuickStartsInTimeRange(LocalTime lowerBound, LocalTime upperBound) { + List memberJpaEntities = memberJpaRepository.findMembersForQuickStartsInTimeRange(lowerBound, upperBound); + if (memberJpaEntities == null || memberJpaEntities.isEmpty()) { + return null; + } + return memberJpaEntities.stream().map(memberMapper::toDomainEntity).collect(Collectors.toList()); + } } diff --git a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java index 20f02a2ba..809dae7b3 100644 --- a/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java +++ b/src/main/java/spring/backend/member/infrastructure/persistence/jpa/repository/MemberJpaRepository.java @@ -1,9 +1,11 @@ package spring.backend.member.infrastructure.persistence.jpa.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import spring.backend.member.domain.value.Role; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; +import java.time.LocalTime; import java.util.List; import java.util.UUID; @@ -14,4 +16,13 @@ public interface MemberJpaRepository extends JpaRepository findAllByEmail(String email); boolean existsByNicknameAndRole(String nickname, Role role); + + @Query(""" + select distinct m + from MemberJpaEntity m + join QuickStartJpaEntity q on q.memberId = m.id + where q.startTime between :lowerBound and :upperBound + and m.emailNotification = true + """) + List findMembersForQuickStartsInTimeRange(LocalTime lowerBound, LocalTime upperBound); } From 31c00d2479e3215efc11f59e42f2576b05acf0d8 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 01:58:06 +0900 Subject: [PATCH 395/478] =?UTF-8?q?feat:=20(#151)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=99=80=20=EB=B9=A0=EB=A5=B8=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EA=B0=80=EC=A7=80=EA=B3=A0=20?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=A0=9C=EB=AA=A9=EC=9D=84=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SendQuickStartEmailsScheduler.java | 84 +++++++++++++++---- .../repository/QuickStartRepository.java | 5 ++ .../batch/job/SendQuickStartEmailsJob.java | 81 +++++++++++++----- .../jpa/adapter/QuickStartRepositoryImpl.java | 16 ++++ .../repository/QuickStartJpaRepository.java | 14 ++++ 5 files changed, 162 insertions(+), 38 deletions(-) diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java index 2a702fcab..a14483e86 100644 --- a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java +++ b/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.domain.entity.Member; @@ -13,18 +15,24 @@ import java.time.LocalTime; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Log4j2 public class SendQuickStartEmailsScheduler { + private final QuickStartRepository quickStartRepository; + private final MemberRepository memberRepository; private final EmailUtil emailUtil; private final TemplateEngine templateEngine; + private static final int TIME_INTERVAL_MINUTES = 15; + @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmails() { List receivers = collectEmailReceiversWithinTimeRange(); @@ -36,35 +44,77 @@ public void sendQuickStartEmails() { } } + private void sendEmails(List receivers) { + List receiverIds = receivers.stream() + .map(Member::getId) + .collect(Collectors.toList()); + + List earliestQuickStartsForMembers = collectEarliestQuickStartsForMembers(receiverIds); + + receivers.forEach(receiver -> { + QuickStart earliestQuickStart = findEarliestQuickStartForReceiver(receiver, earliestQuickStartsForMembers); + if (earliestQuickStart != null) { + sendEmail(receiver, earliestQuickStart); + } else { + log.warn("[SendQuickStartEmailsScheduler] No quick start found for receiver {}", receiver); + } + }); + } + private List collectEmailReceiversWithinTimeRange() { - final int TIME_INTERVAL_MINUTES = 15; - LocalTime now = LocalTime.now(); - LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); - LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + LocalTime lowerBound = getLowerBound(); + LocalTime upperBound = getUpperBound(lowerBound); log.info("[SendQuickStartEmailsScheduler] Searching for receivers between {} and {}", lowerBound, upperBound); return memberRepository.findMembersForQuickStartsInTimeRange(lowerBound, upperBound); } - private void sendEmails(List receivers) { - for (Member receiver : receivers) { - SendEmailRequest request = SendEmailRequest.builder() - .title("Test Title") - .content(generateEmailContent()) - .receiver(receiver.getEmail()) - .build(); - try { - emailUtil.send(request); - log.info("[SendQuickStartEmailsScheduler] Successfully sent email to {}", receiver); - } catch (Exception e) { - log.error("[SendQuickStartEmailsScheduler] Failed to send email to {}", receiver, e); - } + private List collectEarliestQuickStartsForMembers(List receiverIds) { + LocalTime lowerBound = getLowerBound(); + LocalTime upperBound = getUpperBound(lowerBound); + + return quickStartRepository.findEarliestQuickStartsForMembers(receiverIds, lowerBound, upperBound); + } + + private QuickStart findEarliestQuickStartForReceiver(Member receiver, List earliestQuickStartsForMembers) { + return earliestQuickStartsForMembers.stream() + .filter(quickStart -> receiver.getId().equals(quickStart.getMemberId())) + .findFirst() + .orElse(null); + } + + private void sendEmail(Member receiver, QuickStart earliestQuickStart) { + String title = generateEmailTitle(receiver, earliestQuickStart); + SendEmailRequest request = SendEmailRequest.builder() + .title(title) + .content(generateEmailContent()) + .receiver(receiver.getEmail()) + .build(); + + try { + emailUtil.send(request); + log.info("[SendQuickStartEmailsScheduler] Successfully sent email to {}", receiver); + } catch (Exception e) { + log.error("[SendQuickStartEmailsScheduler] Failed to send email to {}", receiver, e); } } + private String generateEmailTitle(Member receiver, QuickStart earliestQuickStart) { + return "%s 님, %s의 시간 조각을 모으러 갈 시간이에요 ⏰".formatted(receiver.getNickname(), earliestQuickStart.getName()); + } + private String generateEmailContent() { Context context = new Context(); return templateEngine.process("mail", context); } + + private LocalTime getLowerBound() { + LocalTime now = LocalTime.now(); + return now.plusMinutes(1).withSecond(0).withNano(0); + } + + private LocalTime getUpperBound(LocalTime lowerBound) { + return lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + } } diff --git a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java index 5e483a366..6b1dd2b81 100644 --- a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java +++ b/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java @@ -2,8 +2,13 @@ import spring.backend.activity.domain.entity.QuickStart; +import java.time.LocalTime; +import java.util.List; +import java.util.UUID; + public interface QuickStartRepository { QuickStart findById(Long id); QuickStart save(QuickStart quickStart); + List findEarliestQuickStartsForMembers(List memberIds, LocalTime lowerBound, LocalTime upperBound); } diff --git a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java index bca11f629..d426d0874 100644 --- a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java +++ b/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java @@ -16,6 +16,8 @@ import org.springframework.transaction.PlatformTransactionManager; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; @@ -23,7 +25,10 @@ import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.*; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; @Configuration @RequiredArgsConstructor @@ -36,13 +41,17 @@ public class SendQuickStartEmailsJob { private final PlatformTransactionManager platformTransactionManager; + private final QuickStartJpaRepository quickStartJpaRepository; + private final MemberJpaRepository memberJpaRepository; private final EmailUtil emailUtil; private final TemplateEngine templateEngine; -// @Scheduled(cron = "0 0/15 * * * ?") + private static final int TIME_INTERVAL_MINUTES = 15; + + // @Scheduled(cron = "0 0/15 * * * ?") public void sendQuickStartEmailsJobScheduler() { try { JobParameters jobParameters = new JobParametersBuilder() @@ -72,9 +81,9 @@ public Tasklet sendQuickStartEmailsTasklet() { return (contribution, chunkContext) -> { try { List receivers = collectEmailReceiversWithinTimeRange(); - if (!receivers.isEmpty()) { - sendEmailsToReceivers(receivers); + List earliestQuickStarts = collectEarliestQuickStartsForMembers(receivers); + sendEmailsToReceivers(receivers, earliestQuickStarts); } else { log.warn("[SendQuickStartEmailsJob] No valid receiver found in the time range."); } @@ -86,34 +95,64 @@ public Tasklet sendQuickStartEmailsTasklet() { } private List collectEmailReceiversWithinTimeRange() { - final int TIME_INTERVAL_MINUTES = 15; - LocalTime now = LocalTime.now(); - LocalTime lowerBound = now.plusMinutes(1).withSecond(0).withNano(0); - LocalTime upperBound = lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + LocalTime lowerBound = getLowerBound(); + LocalTime upperBound = getUpperBound(lowerBound); - log.info("[SendQuickStartEmailsJob] Searching for receivers between {} and {}", lowerBound, upperBound); + return memberJpaRepository.findMembersForQuickStartsInTimeRange(getLowerBound(), upperBound); + } - return memberJpaRepository.findMembersForQuickStartsInTimeRange(lowerBound, upperBound); + private List collectEarliestQuickStartsForMembers(List receivers) { + LocalTime lowerBound = getLowerBound(); + LocalTime upperBound = getUpperBound(lowerBound); + + List receiverIds = receivers.stream() + .map(MemberJpaEntity::getId) + .collect(Collectors.toList()); + return quickStartJpaRepository.findEarliestQuickStartsForMembers(receiverIds, lowerBound, upperBound); } - private void sendEmailsToReceivers(List receivers) { + private void sendEmailsToReceivers(List receivers, List earliestQuickStarts) { for (MemberJpaEntity receiver : receivers) { - SendEmailRequest request = SendEmailRequest.builder() - .title("Test Title") - .content(generateEmailContent()) - .receiver(receiver.getEmail()) - .build(); - try { - emailUtil.send(request); - log.info("[SendQuickStartEmailsJob] Successfully sent email to {}", receiver); - } catch (Exception e) { - log.error("[SendQuickStartEmailsJob] Failed to send email to {}", receiver, e); + QuickStartJpaEntity earliestQuickStart = findEarliestQuickStartForReceiver(receiver, earliestQuickStarts); + if (earliestQuickStart != null) { + String title = generateEmailTitle(receiver, earliestQuickStart); + SendEmailRequest request = SendEmailRequest.builder() + .title(title) + .content(generateEmailContent()) + .receiver(receiver.getEmail()) + .build(); + try { + emailUtil.send(request); + log.info("[SendQuickStartEmailsJob] Successfully sent email to {}", receiver); + } catch (Exception e) { + log.error("[SendQuickStartEmailsJob] Failed to send email to {}", receiver, e); + } } } } + private QuickStartJpaEntity findEarliestQuickStartForReceiver(MemberJpaEntity receiver, List earliestQuickStarts) { + return earliestQuickStarts.stream() + .filter(q -> q.getMemberId().equals(receiver.getId())) + .findFirst() + .orElse(null); + } + + private String generateEmailTitle(MemberJpaEntity receiver, QuickStartJpaEntity earliestQuickStart) { + return "%s 님, %s의 시간 조각을 모으러 갈 시간이에요 ⏰".formatted(receiver.getNickname(), earliestQuickStart.getName()); + } + private String generateEmailContent() { Context context = new Context(); return templateEngine.process("mail", context); } + + private LocalTime getLowerBound() { + LocalTime now = LocalTime.now(); + return now.plusMinutes(1).withSecond(0).withNano(0); + } + + private LocalTime getUpperBound(LocalTime lowerBound) { + return lowerBound.plusMinutes(TIME_INTERVAL_MINUTES - 1); + } } diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java index 1545e6f62..06c61d4f0 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java @@ -10,6 +10,11 @@ import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; +import java.time.LocalTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + @Repository @RequiredArgsConstructor @Log4j2 @@ -38,4 +43,15 @@ public QuickStart save(QuickStart quickStart) { throw QuickStartErrorCode.QUICK_START_SAVE_FAILED.toException(); } } + + @Override + public List findEarliestQuickStartsForMembers(List memberIds, LocalTime lowerBound, LocalTime upperBound) { + List quickStartJpaEntities = quickStartJpaRepository.findEarliestQuickStartsForMembers(memberIds, lowerBound, upperBound); + if (quickStartJpaEntities == null || quickStartJpaEntities.isEmpty()) { + return null; + } + return quickStartJpaEntities.stream() + .map(quickStartMapper::toDomainEntity) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java index 548dafd6c..5b93633c0 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java @@ -1,7 +1,21 @@ package spring.backend.activity.infrastructure.persistence.jpa.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import java.time.LocalTime; +import java.util.List; +import java.util.UUID; + public interface QuickStartJpaRepository extends JpaRepository { + + @Query(""" + select q + from QuickStartJpaEntity q + where q.memberId in :memberIds + and q.startTime between :lowerBound and :upperBound + order by q.startTime asc + """) + List findEarliestQuickStartsForMembers(List memberIds, LocalTime lowerBound,LocalTime upperBound); } From c7eee7b821ede5f118c4862a63ba6dca9ae1e374 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 27 Nov 2024 14:47:47 +0900 Subject: [PATCH 396/478] =?UTF-8?q?refactor:=20(#155)=20Clova=20Prompt?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/dto/request/ClovaStudioPrompt.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index f59a9fe52..bcc55b4a0 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -33,9 +33,6 @@ public class ClovaStudioPrompt { 5. HEALTH - 정의: 신체적, 정신적 건강을 개선하고 유지하기 위한 활동, 스포츠 중심 - 예시: 스트레칭하기, 명상하기, 근력운동하기 등 - 6. SOCIAL - - 정의: 사회적 관계 형성과 유지를 위한 활동, 사람들과의 교류와 유대감 - - 예시: SNS 활동하기, 사람들과 소식 공유하기, 사람들에게 연락하기 등 --- 출력 형식: 원하는 활동 타입 == OFFLINE, ONLINE_AND_OFFLINE: From 497bbe9170045791797b575c930248d967f444c8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 27 Nov 2024 14:48:45 +0900 Subject: [PATCH 397/478] =?UTF-8?q?refactor:=20(#155)=20Keyword=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20NATURE=EB=A5=BC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/domain/value/Keyword.java | 1 - ...itiesByMemberAndKeywordInMonthRequest.java | 4 ++-- ...tiesByMemberAndKeywordInMonthResponse.java | 2 +- .../dto/request/AIRecommendationRequest.java | 2 +- ...etRecommendationsFromClovaServiceTest.java | 19 +------------------ 5 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/main/java/spring/backend/activity/domain/value/Keyword.java b/src/main/java/spring/backend/activity/domain/value/Keyword.java index 4adb2ccb1..72f9bf248 100644 --- a/src/main/java/spring/backend/activity/domain/value/Keyword.java +++ b/src/main/java/spring/backend/activity/domain/value/Keyword.java @@ -23,7 +23,6 @@ public class Keyword { public enum Category { SELF_DEVELOPMENT("자기개발"), HEALTH("건강"), - NATURE("자연"), CULTURE_ART("문화/예술"), ENTERTAINMENT("엔터테인먼트"), RELAXATION("휴식"), diff --git a/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java b/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java index 9371f3e79..cf6e074e2 100644 --- a/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java +++ b/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java @@ -15,8 +15,8 @@ public record ActivitiesByMemberAndKeywordInMonthRequest( int month, @NotNull(message = "키워드 카테고리는 필수 값입니다.") - @Schema(description = "키워드 카테고리 (예: NATURE, HEALTH, SELF_DEVELOPMENT, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", example = "NATURE", allowableValues = { - "SELF_DEVELOPMENT", "HEALTH", "NATURE", "CULTURE_ART", + @Schema(description = "키워드 카테고리 (예: HEALTH, SELF_DEVELOPMENT, CULTURE_ART, ENTERTAINMENT, RELAXATION, SOCIAL)", example = "RELAXATION", allowableValues = { + "SELF_DEVELOPMENT", "HEALTH", "CULTURE_ART", "ENTERTAINMENT", "RELAXATION", "SOCIAL" }) Keyword.Category keywordCategory diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java index 05e6a38bd..468c6a6d0 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -15,7 +15,7 @@ public record ActivitiesByMemberAndKeywordInMonthResponse( @Schema(description = "활동 키워드별 활동 목록") List activities, - @Schema(description = "키워드", example = "{\"category\":\"NATURE\",\"image\":\"https://d1zjcuqflbd5k.cloudfront.net/files/acc_1160/0/1160-2019-07-02-14-07-52-0000.jpg\"}") + @Schema(description = "키워드", example = "{\"category\":\"RELAXATION\",\"image\":\"https://d1zjcuqflbd5k.cloudfront.net/files/acc_1160/0/1160-2019-07-02-14-07-52-0000.jpg\"}") Keyword keyword ) { } diff --git a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java index c68de118d..f5c7ce01a 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java @@ -19,7 +19,7 @@ public record AIRecommendationRequest( Type activityType, @NotNull(message = "키워드는 필수 입력 항목입니다.") - @Schema(description = "활동 키워드", example = "[\"NATURE\",\"CULTURE_ART\"]") + @Schema(description = "활동 키워드", example = "[\"RELAXATION\",\"CULTURE_ART\"]") Keyword.Category[] keywords, @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") diff --git a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java index b79f79a82..bfa1f3157 100644 --- a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java +++ b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java @@ -19,23 +19,6 @@ public class GetRecommendationsFromClovaServiceTest { @Autowired GetRecommendationsFromClovaService getRecommendationsFromClovaService; - @Test - @DisplayName("타입이 ONLINE인데 Keywords에 NATURE가 있는 경우 예외를 반환한다.") - void throwExceptionIfOnlineActivityContainsNatureKeyword() { - // GIVEN - AIRecommendationRequest request = new AIRecommendationRequest( - 300, - Type.ONLINE, - new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, - null - ); - // WHEN - DomainException ex = assertThrows(DomainException.class, () -> getRecommendationsFromClovaService.getRecommendationsFromClova(request)); - - // THEN - assertEquals(ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.name(), ex.getCode()); - } - @Test @DisplayName("타입이 OFFLINE인데 Keywords에 SOCIAL가 있는 경우 예외를 반환한다.") void throwExceptionIfOfflineActivityContainsSocialKeyword() { @@ -43,7 +26,7 @@ void throwExceptionIfOfflineActivityContainsSocialKeyword() { AIRecommendationRequest request = new AIRecommendationRequest( 300, Type.OFFLINE, - new Keyword.Category[]{Keyword.Category.NATURE, Keyword.Category.SOCIAL}, + new Keyword.Category[]{Keyword.Category.RELAXATION, Keyword.Category.SOCIAL}, "서울시 강남구" ); // WHEN From 0cf3abcd18ff83652ab3518d5f6f9df441162bbb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 27 Nov 2024 14:51:35 +0900 Subject: [PATCH 398/478] =?UTF-8?q?refactor:=20(#155)=20Keyword=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.=20-=20ONLINE=5FAND=5FOFFLINE=20?= =?UTF-8?q?=EC=8B=9C=20OFFLINE=20=EC=9A=94=EC=B2=AD=EC=97=90=20SOCIAL?= =?UTF-8?q?=EC=9D=B4=20=EB=93=A4=EC=96=B4=EC=98=AC=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EA=B1=B8=EB=9F=AC=EB=82=B8=EB=8B=A4.=20-=20ClovaStudio=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=EC=84=9C=20=ED=8C=8C=EC=8B=B1?= =?UTF-8?q?=EC=9A=A9=20PATTERN=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20-=20=EC=9D=91=EB=8B=B5=EC=9D=98=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EA=B0=80=20=EC=97=AC=EB=9F=AC=EA=B0=9C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EC=9A=B0,=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=84=EB=A6=AC=ED=95=98=EA=B3=A0=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=ED=95=9C=20=ED=95=98=EB=82=98=EC=9D=98=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EB=A7=8C=20=EC=A0=84=EB=8B=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index f1ef678a7..738d95c3c 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Random; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -27,11 +28,11 @@ public class GetRecommendationsFromClovaService { private static final int MAX_ATTEMPTS = 1; - private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title :.*"); - private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title :"); - private static final Pattern PLACE_NAME_PREFIX_PATTERN = Pattern.compile(".*placeName :"); - private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); - private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword :"); + private static final Pattern TITLE_FULL_LINE_PATTERN = Pattern.compile(".*title\\s*:.*"); + private static final Pattern TITLE_PREFIX_PATTERN = Pattern.compile(".*title\\s*:"); + private static final Pattern PLACE_NAME_PREFIX_PATTERN = Pattern.compile(".*placeName\\s*:"); + private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content\\s*:"); + private static final Pattern KEYWORD_PREFIX_PATTERN = Pattern.compile(".*keyword\\s*:"); private static final String LINE_SEPARATOR = "\n"; private static final int ONLINE_AND_OFFLINE_RECOMMENDATION_COUNT = 3; @@ -72,8 +73,9 @@ private List filteredValidRecommendations(List fetchRecommendations(AIRecommendationRequest clovaRecommendationRequest) { - validateClovaRecommendationRequestKeyword(clovaRecommendationRequest); - ClovaResponse clovaResponse = recommendationProvider.getRecommendations(clovaRecommendationRequest); + AIRecommendationRequest filteredClovaRecommendationRequest = filteredValidRecommendations(clovaRecommendationRequest); + validateClovaRecommendationRequestKeyword(filteredClovaRecommendationRequest); + ClovaResponse clovaResponse = recommendationProvider.getRecommendations(filteredClovaRecommendationRequest); validateClovaResponse(clovaResponse); String parsedClovaResponse = clovaResponse.getResult().getMessage().getContent(); String[] recommendations = parsedClovaResponse.split(LINE_SEPARATOR); @@ -115,7 +117,8 @@ private List fetchRecommendations(AIRecommendationR Keyword keyword = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); - Category category = convertClovaResponseKeywordToKeywordCategory(keywordText); + String parsedKeywordText = parsedKeywordText(keywordText); + Category category = convertClovaResponseKeywordToKeywordCategory(parsedKeywordText); keyword = Keyword.create(category, imageConverter.convertToImageUrl(category)); i++; } @@ -127,6 +130,47 @@ private List fetchRecommendations(AIRecommendationR return clovaResponses; } + private String parsedKeywordText(String keywordText) { + if (keywordText == null || keywordText.isEmpty()) { + return null; + } + + List validKeywords = Arrays.stream(keywordText.split(",")) + .filter(this::isValidKeyword) + .toList(); + + if (validKeywords.isEmpty()) { + return null; + } + + int randomIdx = new Random().nextInt(validKeywords.size()); + return validKeywords.get(randomIdx); + } + + private boolean isValidKeyword(String keyword) { + return Arrays.stream(Keyword.Category.values()) + .map(Keyword.Category::getDescription) + .collect(Collectors.toSet()) + .contains(keyword.trim()); + } + + private AIRecommendationRequest filteredValidRecommendations(AIRecommendationRequest clovaRecommendationRequest) { + if (clovaRecommendationRequest.keywords() == null) { + return clovaRecommendationRequest; + } + + Keyword.Category[] filteredKeywords = Arrays.stream(clovaRecommendationRequest.keywords()) + .filter(category -> category != Keyword.Category.SOCIAL) + .toArray(Keyword.Category[]::new); + + return new AIRecommendationRequest( + clovaRecommendationRequest.spareTime(), + clovaRecommendationRequest.activityType(), + filteredKeywords, + clovaRecommendationRequest.location() + ); + } + private void validateLocation(AIRecommendationRequest clovaRecommendationRequest) { if ((clovaRecommendationRequest.activityType() == OFFLINE || clovaRecommendationRequest.activityType() == ONLINE_AND_OFFLINE) && (clovaRecommendationRequest.location() == null || clovaRecommendationRequest.location().isEmpty())) { @@ -158,10 +202,6 @@ private boolean isValidKeywordCategory(Category keywordCategory) { } private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest clovaRecommendationRequest) { - if (clovaRecommendationRequest.activityType().equals(ONLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Category.NATURE) - ) { - throw ClovaErrorCode.ONLINE_TYPE_CONTAIN_NATURE.toException(); - } if (clovaRecommendationRequest.activityType().equals(OFFLINE) && Arrays.asList(clovaRecommendationRequest.keywords()).contains(Category.SOCIAL) ) { throw ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.toException(); @@ -169,6 +209,9 @@ private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest c } private Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { + if (keywordText == null || keywordText.isEmpty()) { + return null; + } try { return Category.valueOf(keywordText); } catch (IllegalArgumentException e) { From 1dbf01b03266eb8eb9d6ae5406944ac2bc9da2ca Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 13:40:12 +0900 Subject: [PATCH 399/478] =?UTF-8?q?fix:=20(#155)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/exception/ClovaErrorCode.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java index b7b731682..bce13115e 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/exception/ClovaErrorCode.java @@ -13,7 +13,6 @@ public enum ClovaErrorCode implements BaseErrorCode { NO_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 응답이 없습니다."), NULL_RESPONSE_FROM_CLOVA(HttpStatus.INTERNAL_SERVER_ERROR, "클로바 서버로부터 NULL값을 받았습니다."), - ONLINE_TYPE_CONTAIN_NATURE(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 ONLINE인 경우, NATURE(자연) 키워드를 사용할 수 없습니다."), OFFLINE_TYPE_CONTAIN_SOCIAL(HttpStatus.BAD_REQUEST, "선호하는 활동 타입이 OFFLINE인 경우, SOCIAL(소셜) 키워드를 사용할 수 없습니다."), INVALID_KEYWORD_IN_RECOMMENDATIONS(HttpStatus.BAD_REQUEST, "추천 활동의 키워드가 올바르지 않습니다."); From 8a67b77bb1a5e2004279ba1c40b8e9940a7d79d8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 13:41:02 +0900 Subject: [PATCH 400/478] =?UTF-8?q?fix:=20(#155)=20NATURE=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/dto/response/UserMonthlyActivityDetail.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java index c9cea3d3e..354027e76 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java +++ b/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java @@ -7,7 +7,7 @@ public record UserMonthlyActivityDetail( - @Schema(description = "활동 카테고리 \n\n SELF_DEVELOPMENT(자기개발), HEALTH(건강), NATURE(자연), CULTURE_ART(문화/예술), ENTERTAINMENT(엔터테인먼트), RELAXATION(휴식), SOCIAL(소셜)", + @Schema(description = "활동 카테고리 \n\n SELF_DEVELOPMENT(자기개발), HEALTH(건강), CULTURE_ART(문화/예술), ENTERTAINMENT(엔터테인먼트), RELAXATION(휴식), SOCIAL(소셜)", example = "SELF_DEVELOPMENT") Category category, From 4cd71ffd6e963b89136332c997e60a7ee7fe4c70 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 13:41:46 +0900 Subject: [PATCH 401/478] =?UTF-8?q?fix:=20(#155)=20new=20Random=EC=9D=84?= =?UTF-8?q?=20final=EB=A1=9C=20=EC=84=A0=EC=96=B8=ED=95=98=EA=B3=A0,=20par?= =?UTF-8?q?sedKeywordText=20=EB=82=B4=20=EC=B6=94=EA=B0=80=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20trim()=EC=9C=BC=EB=A1=9C=20=EC=9D=98=EB=8F=84?= =?UTF-8?q?=EC=B9=98=EC=95=8A=EC=9D=80=20=EA=B3=B5=EB=B0=B1=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 738d95c3c..d6ea63368 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -39,6 +39,7 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; private final PlaceInfoProvider kakaomapPlaceInfoProvider; private final ImageConverter imageConverter; + private final Random RANDOM = new Random(); public List getRecommendationsFromClova(AIRecommendationRequest clovaRecommendationRequest) { validateLocation(clovaRecommendationRequest); @@ -136,6 +137,7 @@ private String parsedKeywordText(String keywordText) { } List validKeywords = Arrays.stream(keywordText.split(",")) + .map(String::trim) .filter(this::isValidKeyword) .toList(); @@ -143,7 +145,8 @@ private String parsedKeywordText(String keywordText) { return null; } - int randomIdx = new Random().nextInt(validKeywords.size()); + RANDOM.setSeed(System.nanoTime()); + int randomIdx = RANDOM.nextInt(validKeywords.size()); return validKeywords.get(randomIdx); } From a3fecb3da81386bb43a0d82acbab4f21577e711e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 14:28:42 +0900 Subject: [PATCH 402/478] =?UTF-8?q?fix:=20(#155)=20static=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index d6ea63368..c7ef72f31 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -39,7 +39,7 @@ public class GetRecommendationsFromClovaService { private final RecommendationProvider recommendationProvider; private final PlaceInfoProvider kakaomapPlaceInfoProvider; private final ImageConverter imageConverter; - private final Random RANDOM = new Random(); + private static final Random RANDOM = new Random(); public List getRecommendationsFromClova(AIRecommendationRequest clovaRecommendationRequest) { validateLocation(clovaRecommendationRequest); From a752560bb7ee86fd683022b64276a3da78722c91 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 15:07:20 +0900 Subject: [PATCH 403/478] =?UTF-8?q?fix:=20(#162)=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EB=8B=A8=20=EA=B1=B4=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20SendEmailReques?= =?UTF-8?q?t=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=98=EB=A9=B4=EC=84=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=EB=8F=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/spring/backend/core/util/EmailUtilTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/spring/backend/core/util/EmailUtilTest.java b/src/test/java/spring/backend/core/util/EmailUtilTest.java index b800f3350..1736fc5ff 100644 --- a/src/test/java/spring/backend/core/util/EmailUtilTest.java +++ b/src/test/java/spring/backend/core/util/EmailUtilTest.java @@ -24,7 +24,7 @@ public class EmailUtilTest { @Test void throwExceptionWhenToInRequestIsInvalid() { // GIVEN - sendEmailRequest = new SendEmailRequest(new String[]{"test", "test2"}, "Test Subject", "Test Content"); + sendEmailRequest = new SendEmailRequest("test", "Test Subject", "Test Content"); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "올바르지 않은 이메일 주소입니다."); @@ -48,7 +48,7 @@ void throwExceptionWhenToInRequestIsNull() { @Test void throwExceptionWhenSubjectInRequestIsNull() { // GIVEN - sendEmailRequest = new SendEmailRequest(new String[]{"test@naver.com", "test2@naver.com"}, "", "Test Content"); + sendEmailRequest = new SendEmailRequest("test@naver.com", "", "Test Content"); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 제목이 없습니다."); @@ -60,7 +60,7 @@ void throwExceptionWhenSubjectInRequestIsNull() { @Test void throwExceptionWhenTextInRequestIsNull() { // GIVEN - sendEmailRequest = new SendEmailRequest(new String[]{"test@naver.com", "test2@naver.com"}, "Test Subject", ""); + sendEmailRequest = new SendEmailRequest("test@naver.com", "Test Subject", ""); // WHEN & THEN DomainException ex = assertThrows(DomainException.class, () -> emailUtil.send(sendEmailRequest), "메일 내용이 없습니다."); From 9d3989d70f78d6684f7b30f81f3cd8a702da1400 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 14:41:56 +0900 Subject: [PATCH 404/478] =?UTF-8?q?fix:=20(#159)=20=EC=A1=B0=EA=B0=81=20?= =?UTF-8?q?=EB=B3=84=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=EB=8A=94=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B8=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadActivitiesByMemberAndKeywordInMonthService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index cc1f73625..a1049f43a 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -29,7 +29,7 @@ public ActivitiesByMemberAndKeywordInMonthResponse readActivitiesByMemberAndKeyw LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); List activities = activityDao.findActivitiesByMemberAndKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); TotalSavedTimeAndActivityCountByKeywordInMonth totalSavedTimeAndActivityCountByKeywordInMonth = activityDao.findTotalSavedTimeAndActivityCountByKeywordInMonth(member.getId(), firstDayOfMonth, endDayOfMonth, keywordCategory); - Keyword keyword = Keyword.create(keywordCategory, imageConverter.convertToTransparent30ImageUrl(keywordCategory)); + Keyword keyword = Keyword.create(keywordCategory, imageConverter.convertToImageUrl(keywordCategory)); return new ActivitiesByMemberAndKeywordInMonthResponse( totalSavedTimeAndActivityCountByKeywordInMonth.totalSavedTimeByKeywordInMonth(), totalSavedTimeAndActivityCountByKeywordInMonth.totalActivityCountByKeywordInMonth(), From 2199223123706bae8dd251ec1e30fb554dc13835 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 14:54:22 +0900 Subject: [PATCH 405/478] =?UTF-8?q?fix:=20(#160)=20=EC=A1=B0=EA=B0=81=20?= =?UTF-8?q?=EB=B3=84=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=ED=99=9C=EB=8F=99=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=8A=94=20=ED=88=AC=EB=AA=85?= =?UTF-8?q?=EB=8F=84=2030=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadMonthlyActivityOverviewService.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index 937faa754..f435914f7 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -4,11 +4,14 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; import spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse; import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; +import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; import spring.backend.activity.query.dao.ActivityDao; +import spring.backend.core.converter.ImageConverter; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; @@ -24,12 +27,22 @@ public class ReadMonthlyActivityOverviewService { private final ActivityDao activityDao; + private final ImageConverter imageConverter; + public MonthlyActivityOverviewResponse readMonthlyActivityOverview(Member member, MonthlyActivityOverviewRequest monthlyActivityOverviewRequest) { YearMonth yearMonth = YearMonth.of(monthlyActivityOverviewRequest.year(), monthlyActivityOverviewRequest.month()); LocalDateTime startDayOfMonth = TimeUtil.toStartDayOfMonth(yearMonth); LocalDateTime endDayOfMonth = TimeUtil.toEndDayOfMonth(yearMonth); MonthlySavedTimeAndActivityCountResponse monthlySavedTimeAndActivityCountResponse = activityDao.findMonthlyTotalSavedTimeAndTotalCount(member.getId(), startDayOfMonth, endDayOfMonth); List activityByKeywordSummaryResponses = activityDao.findMonthlyActivitiesByKeywordSummary(member.getId(), startDayOfMonth, endDayOfMonth); - return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getYear(), member.getUpdatedAt().getMonth(), monthlySavedTimeAndActivityCountResponse, activityByKeywordSummaryResponses); + List updatedActivityByKeywordSummaryResponses = activityByKeywordSummaryResponses.stream() + .map(response -> { + Category category = response.keyword().getCategory(); + String imageUrl = imageConverter.convertToTransparent30ImageUrl(category); + KeywordJpaValue updatedKeyword = KeywordJpaValue.create(category, imageUrl); + return new MonthlyActivityCountByKeywordResponse(updatedKeyword, response.activityCount()); + }) + .toList(); + return new MonthlyActivityOverviewResponse(member.getUpdatedAt().getYear(), member.getUpdatedAt().getMonth(), monthlySavedTimeAndActivityCountResponse, updatedActivityByKeywordSummaryResponses); } } From 4c88da7c613cade627bf04ac156b6115d086a272 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 19:21:32 +0900 Subject: [PATCH 406/478] =?UTF-8?q?fix:=20(#166)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...etRecommendationsFromClovaServiceTest.java | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java diff --git a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java b/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java deleted file mode 100644 index bfa1f3157..000000000 --- a/src/test/java/spring/backend/recommendation/application/GetRecommendationsFromClovaServiceTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package spring.backend.recommendation.application; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.domain.value.Type; -import spring.backend.core.exception.DomainException; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -public class GetRecommendationsFromClovaServiceTest { - - @Autowired - GetRecommendationsFromClovaService getRecommendationsFromClovaService; - - @Test - @DisplayName("타입이 OFFLINE인데 Keywords에 SOCIAL가 있는 경우 예외를 반환한다.") - void throwExceptionIfOfflineActivityContainsSocialKeyword() { - // GIVEN - AIRecommendationRequest request = new AIRecommendationRequest( - 300, - Type.OFFLINE, - new Keyword.Category[]{Keyword.Category.RELAXATION, Keyword.Category.SOCIAL}, - "서울시 강남구" - ); - // WHEN - DomainException ex = assertThrows(DomainException.class, () -> getRecommendationsFromClovaService.getRecommendationsFromClova(request)); - - // THEN - assertEquals(ClovaErrorCode.OFFLINE_TYPE_CONTAIN_SOCIAL.name(), ex.getCode()); - } -} From 2708cbb49059200cc2f11f2aeece3dd5be92d97f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 19:55:44 +0900 Subject: [PATCH 407/478] =?UTF-8?q?refactor:=20(#168)=20gradle=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EC=97=AD=ED=95=A0=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 77 ++++++------------------------------------ gradle/db.gradle | 10 ++++++ gradle/email.gradle | 4 +++ gradle/jwt.gradle | 6 ++++ gradle/lombok.gradle | 5 +++ gradle/monitor.gradle | 5 +++ gradle/rabbitmq.gradle | 5 +++ gradle/spring.gradle | 25 ++++++++++++++ gradle/swagger.gradle | 4 +++ gradle/test.gradle | 10 ++++++ gradle/webflux.gradle | 7 ++++ 11 files changed, 91 insertions(+), 67 deletions(-) create mode 100644 gradle/db.gradle create mode 100644 gradle/email.gradle create mode 100644 gradle/jwt.gradle create mode 100644 gradle/lombok.gradle create mode 100644 gradle/monitor.gradle create mode 100644 gradle/rabbitmq.gradle create mode 100644 gradle/spring.gradle create mode 100644 gradle/swagger.gradle create mode 100644 gradle/test.gradle create mode 100644 gradle/webflux.gradle diff --git a/build.gradle b/build.gradle index daf4788be..086298cdc 100644 --- a/build.gradle +++ b/build.gradle @@ -23,72 +23,15 @@ repositories { mavenCentral() } -dependencies { - // WEB - implementation 'org.springframework.boot:spring-boot-starter-web' +apply from: "gradle/db.gradle" +apply from: "gradle/email.gradle" +apply from: "gradle/jwt.gradle" +apply from: "gradle/lombok.gradle" +apply from: "gradle/monitor.gradle" +apply from: "gradle/rabbitmq.gradle" +apply from: "gradle/spring.gradle" +apply from: "gradle/swagger.gradle" +apply from: "gradle/test.gradle" +apply from: "gradle/webflux.gradle" - // Testing - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation 'com.h2database:h2' - // Development tools (for local development only) - developmentOnly 'org.springframework.boot:spring-boot-devtools' - - // DB, JPA - runtimeOnly 'com.mysql:mysql-connector-j' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - - // Lombok (code simplification) - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - - // Validation (for request/response validation) - implementation 'org.springframework.boot:spring-boot-starter-validation' - - // WebSocket (for real-time communication) - implementation 'org.springframework.boot:spring-boot-starter-websocket' - - // Swagger-UI - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - - // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - - // monitoring (Prometheus) - implementation 'org.springframework.boot:spring-boot-starter-actuator' - runtimeOnly 'io.micrometer:micrometer-registry-prometheus' - - // Redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - - // WebFlux 외부 API 호출 - implementation 'org.springframework.boot:spring-boot-starter-webflux' - - // Netty - implementation "io.netty:netty-resolver-dns-native-macos:4.1.113.Final:osx-aarch_64" - - // Sentry - implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.17.0' - - // Email - implementation 'org.springframework.boot:spring-boot-starter-mail' - - // Rabbit MQ - implementation 'org.springframework.boot:spring-boot-starter-amqp' - testImplementation 'org.springframework.amqp:spring-rabbit-test' - - // Spring Batch - implementation 'org.springframework.boot:spring-boot-starter-batch' - testImplementation 'org.springframework.batch:spring-batch-test' - - // Thymeleaf - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/gradle/db.gradle b/gradle/db.gradle new file mode 100644 index 000000000..6aa84d287 --- /dev/null +++ b/gradle/db.gradle @@ -0,0 +1,10 @@ +dependencies { + // DB, JPA + runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + testImplementation 'com.h2database:h2' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' +} diff --git a/gradle/email.gradle b/gradle/email.gradle new file mode 100644 index 000000000..8445cba75 --- /dev/null +++ b/gradle/email.gradle @@ -0,0 +1,4 @@ +dependencies { + // Email + implementation 'org.springframework.boot:spring-boot-starter-mail' +} diff --git a/gradle/jwt.gradle b/gradle/jwt.gradle new file mode 100644 index 000000000..60dfb9bf9 --- /dev/null +++ b/gradle/jwt.gradle @@ -0,0 +1,6 @@ +dependencies { + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' +} diff --git a/gradle/lombok.gradle b/gradle/lombok.gradle new file mode 100644 index 000000000..1b976a52f --- /dev/null +++ b/gradle/lombok.gradle @@ -0,0 +1,5 @@ +dependencies { + // Lombok (code simplification) + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' +} diff --git a/gradle/monitor.gradle b/gradle/monitor.gradle new file mode 100644 index 000000000..d9592baab --- /dev/null +++ b/gradle/monitor.gradle @@ -0,0 +1,5 @@ +dependencies { + // monitoring (Prometheus) + implementation 'org.springframework.boot:spring-boot-starter-actuator' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' +} diff --git a/gradle/rabbitmq.gradle b/gradle/rabbitmq.gradle new file mode 100644 index 000000000..f42c6e728 --- /dev/null +++ b/gradle/rabbitmq.gradle @@ -0,0 +1,5 @@ +dependencies { + // Rabbit MQ + implementation 'org.springframework.boot:spring-boot-starter-amqp' + testImplementation 'org.springframework.amqp:spring-rabbit-test' +} diff --git a/gradle/spring.gradle b/gradle/spring.gradle new file mode 100644 index 000000000..ed595f552 --- /dev/null +++ b/gradle/spring.gradle @@ -0,0 +1,25 @@ +dependencies { + // WEB + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Development tools (for local development only) + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // Spring Batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + testImplementation 'org.springframework.batch:spring-batch-test' + + // Thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + + // Validation (for request/response validation) + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // WebSocket (for real-time communication) + implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // Sentry + implementation 'io.sentry:sentry-spring-boot-starter-jakarta:7.17.0' + + +} diff --git a/gradle/swagger.gradle b/gradle/swagger.gradle new file mode 100644 index 000000000..bf3507d37 --- /dev/null +++ b/gradle/swagger.gradle @@ -0,0 +1,4 @@ +dependencies { + // Swagger-UI + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' +} diff --git a/gradle/test.gradle b/gradle/test.gradle new file mode 100644 index 000000000..bc787725e --- /dev/null +++ b/gradle/test.gradle @@ -0,0 +1,10 @@ +dependencies { + // Testing + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/webflux.gradle b/gradle/webflux.gradle new file mode 100644 index 000000000..261eda978 --- /dev/null +++ b/gradle/webflux.gradle @@ -0,0 +1,7 @@ +dependencies { + // WebFlux 외부 API 호출 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Netty + implementation "io.netty:netty-resolver-dns-native-macos:4.1.113.Final:osx-aarch_64" +} From 3ff4e2167c8934b9e628db32bd8fd7b6c223d2b6 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 17:03:12 +0900 Subject: [PATCH 408/478] =?UTF-8?q?refactor:=20(#164)=20=EB=B9=A0=EB=A5=B8?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=9D=91=EC=9A=A9=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EA=B4=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ReadQuickStartsService.java | 8 ++++---- .../application/SendQuickStartEmailsScheduler.java | 6 +++--- .../domain/entity/QuickStart.java | 2 +- .../domain/repository/QuickStartRepository.java | 4 ++-- .../domain/service}/CreateQuickStartService.java | 10 +++++----- .../domain/service}/UpdateQuickStartService.java | 10 +++++----- .../dto/request/QuickStartRequest.java | 2 +- .../dto/response/QuickStartResponse.java | 2 +- .../dto/response/QuickStartsResponse.java | 2 +- .../exception/QuickStartErrorCode.java | 2 +- .../batch/job/SendQuickStartEmailsJob.java | 6 +++--- .../infrastructure/mapper/QuickStartMapper.java | 6 +++--- .../jpa/adapter/QuickStartRepositoryImpl.java | 14 +++++++------- .../persistence/jpa/dao/QuickStartJpaDao.java | 12 ++++++------ .../jpa/entity/QuickStartJpaEntity.java | 2 +- .../jpa/repository/QuickStartJpaRepository.java | 4 ++-- .../presentation/CreateQuickStartController.java | 8 ++++---- .../presentation/ReadQuickStartsController.java | 8 ++++---- .../presentation/UpdateQuickStartController.java | 8 ++++---- .../swagger/CreateQuickStartSwagger.java | 6 +++--- .../swagger/ReadQuickStartsSwagger.java | 6 +++--- .../swagger/UpdateQuickStartSwagger.java | 6 +++--- .../query/dao/QuickStartDao.java | 4 ++-- 23 files changed, 69 insertions(+), 69 deletions(-) rename src/main/java/spring/backend/{activity => quickstart}/application/ReadQuickStartsService.java (75%) rename src/main/java/spring/backend/{activity => quickstart}/application/SendQuickStartEmailsScheduler.java (96%) rename src/main/java/spring/backend/{activity => quickstart}/domain/entity/QuickStart.java (95%) rename src/main/java/spring/backend/{activity => quickstart}/domain/repository/QuickStartRepository.java (74%) rename src/main/java/spring/backend/{activity/application => quickstart/domain/service}/CreateQuickStartService.java (82%) rename src/main/java/spring/backend/{activity/application => quickstart/domain/service}/UpdateQuickStartService.java (88%) rename src/main/java/spring/backend/{activity => quickstart}/dto/request/QuickStartRequest.java (97%) rename src/main/java/spring/backend/{activity => quickstart}/dto/response/QuickStartResponse.java (96%) rename src/main/java/spring/backend/{activity => quickstart}/dto/response/QuickStartsResponse.java (82%) rename src/main/java/spring/backend/{activity => quickstart}/exception/QuickStartErrorCode.java (96%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/batch/job/SendQuickStartEmailsJob.java (96%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/mapper/QuickStartMapper.java (86%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java (77%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java (71%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/persistence/jpa/entity/QuickStartJpaEntity.java (91%) rename src/main/java/spring/backend/{activity => quickstart}/infrastructure/persistence/jpa/repository/QuickStartJpaRepository.java (78%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/CreateQuickStartController.java (80%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/ReadQuickStartsController.java (78%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/UpdateQuickStartController.java (80%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/swagger/CreateQuickStartSwagger.java (83%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/swagger/ReadQuickStartsSwagger.java (83%) rename src/main/java/spring/backend/{activity => quickstart}/presentation/swagger/UpdateQuickStartSwagger.java (82%) rename src/main/java/spring/backend/{activity => quickstart}/query/dao/QuickStartDao.java (70%) diff --git a/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java b/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java similarity index 75% rename from src/main/java/spring/backend/activity/application/ReadQuickStartsService.java rename to src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java index 5703b4073..b8c91c405 100644 --- a/src/main/java/spring/backend/activity/application/ReadQuickStartsService.java +++ b/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java @@ -1,13 +1,13 @@ -package spring.backend.activity.application; +package spring.backend.quickstart.application; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import spring.backend.activity.dto.response.QuickStartResponse; -import spring.backend.activity.dto.response.QuickStartsResponse; -import spring.backend.activity.query.dao.QuickStartDao; +import spring.backend.quickstart.dto.response.QuickStartResponse; +import spring.backend.quickstart.dto.response.QuickStartsResponse; +import spring.backend.quickstart.query.dao.QuickStartDao; import spring.backend.member.domain.entity.Member; import java.util.List; diff --git a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java b/src/main/java/spring/backend/quickstart/application/SendQuickStartEmailsScheduler.java similarity index 96% rename from src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java rename to src/main/java/spring/backend/quickstart/application/SendQuickStartEmailsScheduler.java index a14483e86..f33f2f0f4 100644 --- a/src/main/java/spring/backend/activity/application/SendQuickStartEmailsScheduler.java +++ b/src/main/java/spring/backend/quickstart/application/SendQuickStartEmailsScheduler.java @@ -1,4 +1,4 @@ -package spring.backend.activity.application; +package spring.backend.quickstart.application; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -6,8 +6,8 @@ import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.domain.repository.QuickStartRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java b/src/main/java/spring/backend/quickstart/domain/entity/QuickStart.java similarity index 95% rename from src/main/java/spring/backend/activity/domain/entity/QuickStart.java rename to src/main/java/spring/backend/quickstart/domain/entity/QuickStart.java index 75494b5cc..74dff341e 100644 --- a/src/main/java/spring/backend/activity/domain/entity/QuickStart.java +++ b/src/main/java/spring/backend/quickstart/domain/entity/QuickStart.java @@ -1,4 +1,4 @@ -package spring.backend.activity.domain.entity; +package spring.backend.quickstart.domain.entity; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java b/src/main/java/spring/backend/quickstart/domain/repository/QuickStartRepository.java similarity index 74% rename from src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java rename to src/main/java/spring/backend/quickstart/domain/repository/QuickStartRepository.java index 6b1dd2b81..6556d8c32 100644 --- a/src/main/java/spring/backend/activity/domain/repository/QuickStartRepository.java +++ b/src/main/java/spring/backend/quickstart/domain/repository/QuickStartRepository.java @@ -1,6 +1,6 @@ -package spring.backend.activity.domain.repository; +package spring.backend.quickstart.domain.repository; -import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.quickstart.domain.entity.QuickStart; import java.time.LocalTime; import java.util.List; diff --git a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java b/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java similarity index 82% rename from src/main/java/spring/backend/activity/application/CreateQuickStartService.java rename to src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java index e0ecb9f27..75c27d3e8 100644 --- a/src/main/java/spring/backend/activity/application/CreateQuickStartService.java +++ b/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java @@ -1,13 +1,13 @@ -package spring.backend.activity.application; +package spring.backend.quickstart.domain.service; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; -import spring.backend.activity.dto.request.QuickStartRequest; -import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java b/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java similarity index 88% rename from src/main/java/spring/backend/activity/application/UpdateQuickStartService.java rename to src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java index ec8d1ac49..fccddaeb0 100644 --- a/src/main/java/spring/backend/activity/application/UpdateQuickStartService.java +++ b/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java @@ -1,13 +1,13 @@ -package spring.backend.activity.application; +package spring.backend.quickstart.domain.service; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; -import spring.backend.activity.dto.request.QuickStartRequest; -import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java similarity index 97% rename from src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java rename to src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java index 5100835e5..5639d4aa8 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.quickstart.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java b/src/main/java/spring/backend/quickstart/dto/response/QuickStartResponse.java similarity index 96% rename from src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java rename to src/main/java/spring/backend/quickstart/dto/response/QuickStartResponse.java index 8d34f4441..6a7578bb3 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartResponse.java +++ b/src/main/java/spring/backend/quickstart/dto/response/QuickStartResponse.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.quickstart.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Type; diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java b/src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java similarity index 82% rename from src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java rename to src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java index 55d3c9928..fdc97c7f0 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartsResponse.java +++ b/src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.quickstart.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java b/src/main/java/spring/backend/quickstart/exception/QuickStartErrorCode.java similarity index 96% rename from src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java rename to src/main/java/spring/backend/quickstart/exception/QuickStartErrorCode.java index 1969c85e6..a720aeb88 100644 --- a/src/main/java/spring/backend/activity/exception/QuickStartErrorCode.java +++ b/src/main/java/spring/backend/quickstart/exception/QuickStartErrorCode.java @@ -1,4 +1,4 @@ -package spring.backend.activity.exception; +package spring.backend.quickstart.exception; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java b/src/main/java/spring/backend/quickstart/infrastructure/batch/job/SendQuickStartEmailsJob.java similarity index 96% rename from src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java rename to src/main/java/spring/backend/quickstart/infrastructure/batch/job/SendQuickStartEmailsJob.java index d426d0874..8673773ce 100644 --- a/src/main/java/spring/backend/activity/infrastructure/batch/job/SendQuickStartEmailsJob.java +++ b/src/main/java/spring/backend/quickstart/infrastructure/batch/job/SendQuickStartEmailsJob.java @@ -1,4 +1,4 @@ -package spring.backend.activity.infrastructure.batch.job; +package spring.backend.quickstart.infrastructure.batch.job; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -16,8 +16,8 @@ import org.springframework.transaction.PlatformTransactionManager; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; -import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; -import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; +import spring.backend.quickstart.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.quickstart.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; import spring.backend.core.util.email.EmailUtil; import spring.backend.core.util.email.dto.request.SendEmailRequest; import spring.backend.member.infrastructure.persistence.jpa.entity.MemberJpaEntity; diff --git a/src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java b/src/main/java/spring/backend/quickstart/infrastructure/mapper/QuickStartMapper.java similarity index 86% rename from src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java rename to src/main/java/spring/backend/quickstart/infrastructure/mapper/QuickStartMapper.java index dbece55fe..f91174d60 100644 --- a/src/main/java/spring/backend/activity/infrastructure/mapper/QuickStartMapper.java +++ b/src/main/java/spring/backend/quickstart/infrastructure/mapper/QuickStartMapper.java @@ -1,8 +1,8 @@ -package spring.backend.activity.infrastructure.mapper; +package spring.backend.quickstart.infrastructure.mapper; import org.springframework.stereotype.Component; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; import java.util.Optional; diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java b/src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java similarity index 77% rename from src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java rename to src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java index 06c61d4f0..d31ee56db 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java +++ b/src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/adapter/QuickStartRepositoryImpl.java @@ -1,14 +1,14 @@ -package spring.backend.activity.infrastructure.persistence.jpa.adapter; +package spring.backend.quickstart.infrastructure.persistence.jpa.adapter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Repository; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; -import spring.backend.activity.exception.QuickStartErrorCode; -import spring.backend.activity.infrastructure.mapper.QuickStartMapper; -import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; -import spring.backend.activity.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.quickstart.exception.QuickStartErrorCode; +import spring.backend.quickstart.infrastructure.mapper.QuickStartMapper; +import spring.backend.quickstart.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.quickstart.infrastructure.persistence.jpa.repository.QuickStartJpaRepository; import java.time.LocalTime; import java.util.List; diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java b/src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java similarity index 71% rename from src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java rename to src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java index 93f29e158..ca3794dc8 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java +++ b/src/main/java/spring/backend/quickstart/infrastructure/persistence/jpa/dao/QuickStartJpaDao.java @@ -1,11 +1,11 @@ -package spring.backend.activity.infrastructure.persistence.jpa.dao; +package spring.backend.quickstart.infrastructure.persistence.jpa.dao; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import spring.backend.activity.dto.response.QuickStartResponse; -import spring.backend.activity.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; -import spring.backend.activity.query.dao.QuickStartDao; +import spring.backend.quickstart.dto.response.QuickStartResponse; +import spring.backend.quickstart.infrastructure.persistence.jpa.entity.QuickStartJpaEntity; +import spring.backend.quickstart.query.dao.QuickStartDao; import java.util.List; import java.util.UUID; @@ -14,7 +14,7 @@ public interface QuickStartJpaDao extends JpaRepository Date: Thu, 28 Nov 2024 17:10:02 +0900 Subject: [PATCH 409/478] =?UTF-8?q?refactor:=20(#164)=20=ED=99=9C=EB=8F=99?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=9D=91=EC=9A=A9=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EA=B4=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/QuickStartActivitySelectService.java | 5 +++-- .../activity/application/UserActivitySelectService.java | 1 + .../service}/FinishActivityAutoService.java | 2 +- .../service}/FinishActivityService.java | 2 +- .../activity/presentation/FinishActivityController.java | 2 +- .../swagger/QuickStartActivitySelectSwagger.java | 3 +-- 6 files changed, 8 insertions(+), 7 deletions(-) rename src/main/java/spring/backend/activity/{application => domain/service}/FinishActivityAutoService.java (95%) rename src/main/java/spring/backend/activity/{application => domain/service}/FinishActivityService.java (97%) diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java index ce77e14af..e8e3f700d 100644 --- a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -6,11 +6,12 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; -import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; -import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.member.domain.entity.Member; @Service diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index d5443834c..1980dbc13 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -6,6 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.dto.request.UserActivitySelectRequest; import spring.backend.activity.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; diff --git a/src/main/java/spring/backend/activity/application/FinishActivityAutoService.java b/src/main/java/spring/backend/activity/domain/service/FinishActivityAutoService.java similarity index 95% rename from src/main/java/spring/backend/activity/application/FinishActivityAutoService.java rename to src/main/java/spring/backend/activity/domain/service/FinishActivityAutoService.java index b925dfada..08c0572a8 100644 --- a/src/main/java/spring/backend/activity/application/FinishActivityAutoService.java +++ b/src/main/java/spring/backend/activity/domain/service/FinishActivityAutoService.java @@ -1,4 +1,4 @@ -package spring.backend.activity.application; +package spring.backend.activity.domain.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/spring/backend/activity/application/FinishActivityService.java b/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java similarity index 97% rename from src/main/java/spring/backend/activity/application/FinishActivityService.java rename to src/main/java/spring/backend/activity/domain/service/FinishActivityService.java index c8d729f05..5f88b136f 100644 --- a/src/main/java/spring/backend/activity/application/FinishActivityService.java +++ b/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java @@ -1,4 +1,4 @@ -package spring.backend.activity.application; +package spring.backend.activity.domain.service; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; diff --git a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java index 7e9e71efb..df6954a71 100644 --- a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java +++ b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import spring.backend.activity.application.FinishActivityService; +import spring.backend.activity.domain.service.FinishActivityService; import spring.backend.activity.dto.response.FinishActivityResponse; import spring.backend.activity.presentation.swagger.FinishActivitySwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java index c95a6301a..24580f702 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -4,11 +4,10 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; -import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; From dd2246917e7e07f3dc4ba10fa9ba5c9585682e72 Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 17:10:56 +0900 Subject: [PATCH 410/478] =?UTF-8?q?refactor:=20(#164)=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=9D=91=EC=9A=A9=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EA=B4=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 2 +- .../backend/member/application/ReadMemberHomeService.java | 4 ++-- .../service}/CreateMemberWithOAuthService.java | 2 +- .../member/domain/service/EditMemberProfileService.java | 2 -- .../service}/ValidateNicknameService.java | 2 +- .../spring/backend/member/dto/response/HomeMainResponse.java | 2 +- .../member/presentation/ValidateNicknameController.java | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) rename src/main/java/spring/backend/member/{application => domain/service}/CreateMemberWithOAuthService.java (98%) rename src/main/java/spring/backend/member/{application => domain/service}/ValidateNicknameService.java (96%) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index a2d7712dc..4b412d827 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -9,7 +9,7 @@ import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.OAuthRestClientFactory; -import spring.backend.member.application.CreateMemberWithOAuthService; +import spring.backend.member.domain.service.CreateMemberWithOAuthService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Provider; import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; diff --git a/src/main/java/spring/backend/member/application/ReadMemberHomeService.java b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java index 295d6899d..0fdf50faf 100644 --- a/src/main/java/spring/backend/member/application/ReadMemberHomeService.java +++ b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java @@ -5,9 +5,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.dto.response.HomeActivityInfoResponse; -import spring.backend.activity.dto.response.QuickStartResponse; +import spring.backend.quickstart.dto.response.QuickStartResponse; import spring.backend.activity.query.dao.ActivityDao; -import spring.backend.activity.query.dao.QuickStartDao; +import spring.backend.quickstart.query.dao.QuickStartDao; import spring.backend.member.domain.entity.Member; import spring.backend.member.dto.response.HomeMainResponse; import spring.backend.member.dto.response.HomeMemberInfoResponse; diff --git a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java similarity index 98% rename from src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java rename to src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java index c07926a8e..65842341f 100644 --- a/src/main/java/spring/backend/member/application/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java @@ -1,4 +1,4 @@ -package spring.backend.member.application; +package spring.backend.member.domain.service; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 0fe096b0c..4c06f9771 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -4,11 +4,9 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import spring.backend.member.application.ValidateNicknameService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.dto.request.EditMemberProfileRequest; -import spring.backend.member.exception.MemberErrorCode; @Service @RequiredArgsConstructor diff --git a/src/main/java/spring/backend/member/application/ValidateNicknameService.java b/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java similarity index 96% rename from src/main/java/spring/backend/member/application/ValidateNicknameService.java rename to src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java index 13a922726..5d3405712 100644 --- a/src/main/java/spring/backend/member/application/ValidateNicknameService.java +++ b/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java @@ -1,4 +1,4 @@ -package spring.backend.member.application; +package spring.backend.member.domain.service; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; diff --git a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java index da4ed6779..c0e8e1993 100644 --- a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java +++ b/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.dto.response.HomeActivityInfoResponse; -import spring.backend.activity.dto.response.QuickStartResponse; +import spring.backend.quickstart.dto.response.QuickStartResponse; import java.util.List; diff --git a/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java index d4ad8e94f..edf752591 100644 --- a/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java +++ b/src/main/java/spring/backend/member/presentation/ValidateNicknameController.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.core.presentation.RestResponse; -import spring.backend.member.application.ValidateNicknameService; +import spring.backend.member.domain.service.ValidateNicknameService; import spring.backend.member.presentation.swagger.ValidateNicknameSwagger; @RestController From a08f6e1047c8a83b0e75043cf9c0818a6a66ed2b Mon Sep 17 00:00:00 2001 From: anxi01 Date: Thu, 28 Nov 2024 17:16:22 +0900 Subject: [PATCH 411/478] =?UTF-8?q?refactor:=20(#164)=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EA=B4=80=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A5=BC=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuickStartActivitySelectServiceTest.java | 7 ++++--- .../application/UserActivitySelectServiceTest.java | 1 + .../service}/CreateMemberWithOAuthServiceTest.java | 2 +- .../service}/ValidateNicknameServiceTest.java | 2 +- .../domain/repository/QuickStartRepositoryTest.java | 6 +++--- .../domain/service}/CreateQuickStartServiceTest.java | 12 ++++++------ .../dto/request/QuickStartRequestTest.java | 4 ++-- 7 files changed, 18 insertions(+), 16 deletions(-) rename src/test/java/spring/backend/member/{application => domain/service}/CreateMemberWithOAuthServiceTest.java (97%) rename src/test/java/spring/backend/member/{application => domain/service}/ValidateNicknameServiceTest.java (97%) rename src/test/java/spring/backend/{activity => quickstart}/domain/repository/QuickStartRepositoryTest.java (92%) rename src/test/java/spring/backend/{activity/application => quickstart/domain/service}/CreateQuickStartServiceTest.java (89%) rename src/test/java/spring/backend/{activity => quickstart}/dto/request/QuickStartRequestTest.java (98%) diff --git a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java index 46823d72b..decdb1a0a 100644 --- a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java @@ -8,15 +8,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import spring.backend.activity.domain.entity.Activity; -import spring.backend.activity.domain.entity.QuickStart; +import spring.backend.quickstart.domain.entity.QuickStart; import spring.backend.activity.domain.repository.ActivityRepository; -import spring.backend.activity.domain.repository.QuickStartRepository; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; -import spring.backend.activity.exception.QuickStartErrorCode; +import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index 14930e4b7..9d5747e0b 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; +import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.activity.dto.request.UserActivitySelectRequest; diff --git a/src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java b/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java similarity index 97% rename from src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java rename to src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java index 3fc099c3e..09f7c8ec1 100644 --- a/src/test/java/spring/backend/member/application/CreateMemberWithOAuthServiceTest.java +++ b/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java @@ -1,4 +1,4 @@ -package spring.backend.member.application; +package spring.backend.member.domain.service; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java b/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java similarity index 97% rename from src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java rename to src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java index ca690a1b9..6e78e0976 100644 --- a/src/test/java/spring/backend/member/application/ValidateNicknameServiceTest.java +++ b/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java @@ -1,4 +1,4 @@ -package spring.backend.member.application; +package spring.backend.member.domain.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java b/src/test/java/spring/backend/quickstart/domain/repository/QuickStartRepositoryTest.java similarity index 92% rename from src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java rename to src/test/java/spring/backend/quickstart/domain/repository/QuickStartRepositoryTest.java index bcaa19630..230edff1d 100644 --- a/src/test/java/spring/backend/activity/domain/repository/QuickStartRepositoryTest.java +++ b/src/test/java/spring/backend/quickstart/domain/repository/QuickStartRepositoryTest.java @@ -1,11 +1,11 @@ -package spring.backend.activity.domain.repository; +package spring.backend.quickstart.domain.repository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import spring.backend.activity.domain.entity.QuickStart; import spring.backend.activity.domain.value.Type; +import spring.backend.quickstart.domain.entity.QuickStart; import java.time.LocalDateTime; import java.time.LocalTime; @@ -43,4 +43,4 @@ void testSaveAndFindQuickStart() { assertThat(foundQuickStart).isNotNull(); assertThat(foundQuickStart.getStartTime()).isEqualTo(quickStart.getStartTime()); } -} \ No newline at end of file +} diff --git a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java similarity index 89% rename from src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java rename to src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java index 06b11090a..5e149cb53 100644 --- a/src/test/java/spring/backend/activity/application/CreateQuickStartServiceTest.java +++ b/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.application; +package spring.backend.quickstart.domain.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -7,15 +7,15 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import spring.backend.activity.domain.entity.QuickStart; -import spring.backend.activity.domain.repository.QuickStartRepository; import spring.backend.activity.domain.value.Type; -import spring.backend.activity.dto.request.QuickStartRequest; -import spring.backend.activity.exception.QuickStartErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Role; +import spring.backend.quickstart.domain.entity.QuickStart; +import spring.backend.quickstart.domain.repository.QuickStartRepository; +import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.exception.QuickStartErrorCode; import java.time.LocalTime; @@ -76,4 +76,4 @@ public void createQuickStart_ValidRequest_ReturnsSavedQuickStartId() { assertEquals(quickStart.getId(), savedQuickStartId); verify(quickStartRepository).save(any(QuickStart.class)); } -} \ No newline at end of file +} diff --git a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java similarity index 98% rename from src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java rename to src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java index 66f6a19eb..320447836 100644 --- a/src/test/java/spring/backend/activity/dto/request/QuickStartRequestTest.java +++ b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.quickstart.dto.request; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; @@ -71,4 +71,4 @@ void whenNameExceedsMaxLength_thenValidationFails() { assertThat(violations).anyMatch(violation -> violation.getMessage().contains("최대 10자까지 입력 가능합니다.")); } } -} \ No newline at end of file +} From 7983befbe96e83f6e3cb54606a56df32da3668b6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 21:05:20 +0900 Subject: [PATCH 412/478] =?UTF-8?q?refactor:=20(#170)=20Recommendation=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=98=20dto=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20controller=20=EC=82=AC=EC=9A=A9=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 4 ++-- .../application/GetRecommendationsFromOpenAIService.java | 4 ++-- .../application/RecommendationProvider.java | 2 +- .../clova/application/ClovaRecommendationProvider.java | 2 +- .../infrastructure/clova/dto/request/ClovaRequest.java | 2 +- .../recommendation/infrastructure/dto/Message.java | 2 +- .../openai/OpenAIRecommendationProvider.java | 2 +- .../presentation/GetRecommendationsController.java | 8 ++++---- .../dto/request/AIRecommendationRequest.java | 2 +- .../dto/response/ClovaRecommendationResponse.java | 2 +- .../dto/response/OpenAIRecommendationResponse.java | 2 +- .../dto/response/RecommendationResponse.java | 2 +- .../presentation/swagger/GetRecommendationsSwagger.java | 4 ++-- 13 files changed, 19 insertions(+), 19 deletions(-) rename src/main/java/spring/backend/recommendation/{ => presentation}/dto/request/AIRecommendationRequest.java (95%) rename src/main/java/spring/backend/recommendation/{ => presentation}/dto/response/ClovaRecommendationResponse.java (92%) rename src/main/java/spring/backend/recommendation/{ => presentation}/dto/response/OpenAIRecommendationResponse.java (94%) rename src/main/java/spring/backend/recommendation/{ => presentation}/dto/response/RecommendationResponse.java (91%) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index c7ef72f31..d3bb837ce 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -7,8 +7,8 @@ import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.converter.ImageConverter; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.response.ClovaRecommendationResponse; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import spring.backend.recommendation.infrastructure.map.kakao.dto.response.KakaoMapResponse; diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java index 4c12fd120..f748286e4 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromOpenAIService.java @@ -8,8 +8,8 @@ import spring.backend.activity.domain.value.Keyword.Category; import spring.backend.activity.domain.value.Type; import spring.backend.core.converter.ImageConverter; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.response.OpenAIRecommendationResponse; import spring.backend.recommendation.infrastructure.dto.Message; import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse; import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse.Choice; diff --git a/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java index e15d6ac02..8e9d02aa5 100644 --- a/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/application/RecommendationProvider.java @@ -1,6 +1,6 @@ package spring.backend.recommendation.application; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; public interface RecommendationProvider { T getRecommendations(AIRecommendationRequest aiRecommendationRequest); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java index a5c3dd58d..8fc784ea3 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/application/ClovaRecommendationProvider.java @@ -9,7 +9,7 @@ import org.springframework.web.reactive.function.client.WebClientException; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.application.RecommendationProvider; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.clova.dto.request.ClovaRequest; import spring.backend.recommendation.infrastructure.clova.dto.response.ClovaResponse; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java index 873779614..3c7ebdaab 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaRequest.java @@ -2,7 +2,7 @@ import lombok.Builder; import lombok.Getter; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.dto.Message; import java.util.ArrayList; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java index 317dca1ba..fc7ad9e2f 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/dto/Message.java @@ -5,7 +5,7 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; import spring.backend.activity.exception.ActivityErrorCode; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; import java.util.Arrays; import java.util.stream.Collectors; diff --git a/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java b/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java index 89948c400..b533fd375 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/openai/OpenAIRecommendationProvider.java @@ -10,7 +10,7 @@ import reactor.core.publisher.Mono; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.recommendation.application.RecommendationProvider; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; import spring.backend.recommendation.infrastructure.dto.Message; import spring.backend.recommendation.infrastructure.openai.dto.request.OpenAIPrompt; import spring.backend.recommendation.infrastructure.openai.dto.response.OpenAIResponse; diff --git a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java index 9964e9111..71a35f064 100644 --- a/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java +++ b/src/main/java/spring/backend/recommendation/presentation/GetRecommendationsController.java @@ -12,10 +12,10 @@ import spring.backend.member.domain.entity.Member; import spring.backend.recommendation.application.GetRecommendationsFromClovaService; import spring.backend.recommendation.application.GetRecommendationsFromOpenAIService; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.ClovaRecommendationResponse; -import spring.backend.recommendation.dto.response.OpenAIRecommendationResponse; -import spring.backend.recommendation.dto.response.RecommendationResponse; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.response.ClovaRecommendationResponse; +import spring.backend.recommendation.presentation.dto.response.OpenAIRecommendationResponse; +import spring.backend.recommendation.presentation.dto.response.RecommendationResponse; import spring.backend.recommendation.presentation.swagger.GetRecommendationsSwagger; import java.util.ArrayList; diff --git a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java b/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java similarity index 95% rename from src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java rename to src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java index f5c7ce01a..68fbda8dc 100644 --- a/src/main/java/spring/backend/recommendation/dto/request/AIRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java @@ -1,4 +1,4 @@ -package spring.backend.recommendation.dto.request; +package spring.backend.recommendation.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; diff --git a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java b/src/main/java/spring/backend/recommendation/presentation/dto/response/ClovaRecommendationResponse.java similarity index 92% rename from src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java rename to src/main/java/spring/backend/recommendation/presentation/dto/response/ClovaRecommendationResponse.java index 7cce4febf..1c016a691 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/ClovaRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/presentation/dto/response/ClovaRecommendationResponse.java @@ -1,4 +1,4 @@ -package spring.backend.recommendation.dto.response; +package spring.backend.recommendation.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java b/src/main/java/spring/backend/recommendation/presentation/dto/response/OpenAIRecommendationResponse.java similarity index 94% rename from src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java rename to src/main/java/spring/backend/recommendation/presentation/dto/response/OpenAIRecommendationResponse.java index c5a9a791c..1bd08cb99 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/OpenAIRecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/presentation/dto/response/OpenAIRecommendationResponse.java @@ -1,4 +1,4 @@ -package spring.backend.recommendation.dto.response; +package spring.backend.recommendation.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword; diff --git a/src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java b/src/main/java/spring/backend/recommendation/presentation/dto/response/RecommendationResponse.java similarity index 91% rename from src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java rename to src/main/java/spring/backend/recommendation/presentation/dto/response/RecommendationResponse.java index fe3a7b204..c61935a21 100644 --- a/src/main/java/spring/backend/recommendation/dto/response/RecommendationResponse.java +++ b/src/main/java/spring/backend/recommendation/presentation/dto/response/RecommendationResponse.java @@ -1,4 +1,4 @@ -package spring.backend.recommendation.dto.response; +package spring.backend.recommendation.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java index 88a58a9b5..0e2547d69 100644 --- a/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java +++ b/src/main/java/spring/backend/recommendation/presentation/swagger/GetRecommendationsSwagger.java @@ -8,8 +8,8 @@ import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import spring.backend.recommendation.dto.request.AIRecommendationRequest; -import spring.backend.recommendation.dto.response.RecommendationResponse; +import spring.backend.recommendation.presentation.dto.request.AIRecommendationRequest; +import spring.backend.recommendation.presentation.dto.response.RecommendationResponse; import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import spring.backend.recommendation.infrastructure.openai.exception.OpenAIErrorCode; From 75b684a82553b97c69a5037b99e8e224e0a903c5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 21:05:28 +0900 Subject: [PATCH 413/478] =?UTF-8?q?refactor:=20(#170)=20QuickStart=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=98=20dto=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20controller=20=EC=82=AC=EC=9A=A9=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/quickstart/application/ReadQuickStartsService.java | 2 +- .../quickstart/domain/service/CreateQuickStartService.java | 2 +- .../quickstart/domain/service/UpdateQuickStartService.java | 2 +- .../quickstart/presentation/CreateQuickStartController.java | 2 +- .../quickstart/presentation/ReadQuickStartsController.java | 2 +- .../quickstart/presentation/UpdateQuickStartController.java | 2 +- .../{ => presentation}/dto/request/QuickStartRequest.java | 2 +- .../{ => presentation}/dto/response/QuickStartsResponse.java | 3 ++- .../presentation/swagger/CreateQuickStartSwagger.java | 2 +- .../presentation/swagger/ReadQuickStartsSwagger.java | 2 +- .../presentation/swagger/UpdateQuickStartSwagger.java | 2 +- .../quickstart/domain/service/CreateQuickStartServiceTest.java | 2 +- .../backend/quickstart/dto/request/QuickStartRequestTest.java | 1 + 13 files changed, 14 insertions(+), 12 deletions(-) rename src/main/java/spring/backend/quickstart/{ => presentation}/dto/request/QuickStartRequest.java (97%) rename src/main/java/spring/backend/quickstart/{ => presentation}/dto/response/QuickStartsResponse.java (64%) diff --git a/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java b/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java index b8c91c405..48c58a0c5 100644 --- a/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java +++ b/src/main/java/spring/backend/quickstart/application/ReadQuickStartsService.java @@ -6,7 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.quickstart.dto.response.QuickStartResponse; -import spring.backend.quickstart.dto.response.QuickStartsResponse; +import spring.backend.quickstart.presentation.dto.response.QuickStartsResponse; import spring.backend.quickstart.query.dao.QuickStartDao; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java b/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java index 75c27d3e8..3fe25efc3 100644 --- a/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java +++ b/src/main/java/spring/backend/quickstart/domain/service/CreateQuickStartService.java @@ -6,7 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.quickstart.domain.entity.QuickStart; import spring.backend.quickstart.domain.repository.QuickStartRepository; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java b/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java index fccddaeb0..499169878 100644 --- a/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java +++ b/src/main/java/spring/backend/quickstart/domain/service/UpdateQuickStartService.java @@ -6,7 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.quickstart.domain.entity.QuickStart; import spring.backend.quickstart.domain.repository.QuickStartRepository; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.util.TimeUtil; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/quickstart/presentation/CreateQuickStartController.java b/src/main/java/spring/backend/quickstart/presentation/CreateQuickStartController.java index 251203cfb..a5fabde5c 100644 --- a/src/main/java/spring/backend/quickstart/presentation/CreateQuickStartController.java +++ b/src/main/java/spring/backend/quickstart/presentation/CreateQuickStartController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.quickstart.domain.service.CreateQuickStartService; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.presentation.swagger.CreateQuickStartSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/quickstart/presentation/ReadQuickStartsController.java b/src/main/java/spring/backend/quickstart/presentation/ReadQuickStartsController.java index 00917ec4f..3f7c8beb7 100644 --- a/src/main/java/spring/backend/quickstart/presentation/ReadQuickStartsController.java +++ b/src/main/java/spring/backend/quickstart/presentation/ReadQuickStartsController.java @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.quickstart.application.ReadQuickStartsService; -import spring.backend.quickstart.dto.response.QuickStartsResponse; +import spring.backend.quickstart.presentation.dto.response.QuickStartsResponse; import spring.backend.quickstart.presentation.swagger.ReadQuickStartsSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/quickstart/presentation/UpdateQuickStartController.java b/src/main/java/spring/backend/quickstart/presentation/UpdateQuickStartController.java index 004ab2989..5d5cad395 100644 --- a/src/main/java/spring/backend/quickstart/presentation/UpdateQuickStartController.java +++ b/src/main/java/spring/backend/quickstart/presentation/UpdateQuickStartController.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.quickstart.domain.service.UpdateQuickStartService; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.presentation.swagger.UpdateQuickStartSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java similarity index 97% rename from src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java rename to src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java index 5639d4aa8..53f839a4a 100644 --- a/src/main/java/spring/backend/quickstart/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java @@ -1,4 +1,4 @@ -package spring.backend.quickstart.dto.request; +package spring.backend.quickstart.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; diff --git a/src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java b/src/main/java/spring/backend/quickstart/presentation/dto/response/QuickStartsResponse.java similarity index 64% rename from src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java rename to src/main/java/spring/backend/quickstart/presentation/dto/response/QuickStartsResponse.java index fdc97c7f0..a10230b44 100644 --- a/src/main/java/spring/backend/quickstart/dto/response/QuickStartsResponse.java +++ b/src/main/java/spring/backend/quickstart/presentation/dto/response/QuickStartsResponse.java @@ -1,6 +1,7 @@ -package spring.backend.quickstart.dto.response; +package spring.backend.quickstart.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.quickstart.dto.response.QuickStartResponse; import java.util.List; diff --git a/src/main/java/spring/backend/quickstart/presentation/swagger/CreateQuickStartSwagger.java b/src/main/java/spring/backend/quickstart/presentation/swagger/CreateQuickStartSwagger.java index 77514dcd9..43d718169 100644 --- a/src/main/java/spring/backend/quickstart/presentation/swagger/CreateQuickStartSwagger.java +++ b/src/main/java/spring/backend/quickstart/presentation/swagger/CreateQuickStartSwagger.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/main/java/spring/backend/quickstart/presentation/swagger/ReadQuickStartsSwagger.java b/src/main/java/spring/backend/quickstart/presentation/swagger/ReadQuickStartsSwagger.java index b26927457..f38b1df5c 100644 --- a/src/main/java/spring/backend/quickstart/presentation/swagger/ReadQuickStartsSwagger.java +++ b/src/main/java/spring/backend/quickstart/presentation/swagger/ReadQuickStartsSwagger.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.quickstart.dto.response.QuickStartsResponse; +import spring.backend.quickstart.presentation.dto.response.QuickStartsResponse; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/main/java/spring/backend/quickstart/presentation/swagger/UpdateQuickStartSwagger.java b/src/main/java/spring/backend/quickstart/presentation/swagger/UpdateQuickStartSwagger.java index a57a412a8..af8204c9a 100644 --- a/src/main/java/spring/backend/quickstart/presentation/swagger/UpdateQuickStartSwagger.java +++ b/src/main/java/spring/backend/quickstart/presentation/swagger/UpdateQuickStartSwagger.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java b/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java index 5e149cb53..e75c883c1 100644 --- a/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java +++ b/src/test/java/spring/backend/quickstart/domain/service/CreateQuickStartServiceTest.java @@ -14,7 +14,7 @@ import spring.backend.member.domain.value.Role; import spring.backend.quickstart.domain.entity.QuickStart; import spring.backend.quickstart.domain.repository.QuickStartRepository; -import spring.backend.quickstart.dto.request.QuickStartRequest; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import spring.backend.quickstart.exception.QuickStartErrorCode; import java.time.LocalTime; diff --git a/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java index 320447836..b767a8b13 100644 --- a/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java +++ b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import spring.backend.activity.domain.value.Type; +import spring.backend.quickstart.presentation.dto.request.QuickStartRequest; import java.util.Set; From 62d42194cc7d11e34342d3f52488233ff184cd2b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 21:06:56 +0900 Subject: [PATCH 414/478] =?UTF-8?q?refactor:=20(#170)=20Member=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=98=20dto=20=EA=B5=AC=EC=A1=B0=EB=A5=BC?= =?UTF-8?q?=20controller=20=EC=82=AC=EC=9A=A9=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 2 +- .../backend/member/application/ReadMemberHomeService.java | 2 +- .../member/domain/service/CreateMemberWithOAuthService.java | 2 +- .../member/domain/service/EditMemberProfileService.java | 2 +- .../member/presentation/EditMemberProfileController.java | 2 +- .../backend/member/presentation/ReadMemberHomeController.java | 2 +- .../member/presentation/ReadMemberProfileController.java | 2 +- .../dto/request/CreateMemberWithOAuthRequest.java | 2 +- .../dto/request/EditMemberProfileRequest.java | 2 +- .../{ => presentation}/dto/response/HomeMainResponse.java | 3 ++- .../{ => presentation}/dto/response/MemberProfileResponse.java | 2 +- .../member/presentation/swagger/EditMemberProfileSwagger.java | 2 +- .../member/presentation/swagger/ReadMemberHomeSwagger.java | 2 +- .../member/presentation/swagger/ReadMemberProfileSwagger.java | 2 +- .../domain/service/CreateMemberWithOAuthServiceTest.java | 2 +- .../backend/member/dto/response/MemberProfileResponseTest.java | 1 + 16 files changed, 17 insertions(+), 15 deletions(-) rename src/main/java/spring/backend/member/{ => presentation}/dto/request/CreateMemberWithOAuthRequest.java (82%) rename src/main/java/spring/backend/member/{ => presentation}/dto/request/EditMemberProfileRequest.java (92%) rename src/main/java/spring/backend/member/{ => presentation}/dto/response/HomeMainResponse.java (87%) rename src/main/java/spring/backend/member/{ => presentation}/dto/response/MemberProfileResponse.java (90%) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 4b412d827..8e69cfd38 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -12,7 +12,7 @@ import spring.backend.member.domain.service.CreateMemberWithOAuthService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.value.Provider; -import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; +import spring.backend.member.presentation.dto.request.CreateMemberWithOAuthRequest; import spring.backend.core.application.JwtService; @Service diff --git a/src/main/java/spring/backend/member/application/ReadMemberHomeService.java b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java index 0fdf50faf..5da0d7dca 100644 --- a/src/main/java/spring/backend/member/application/ReadMemberHomeService.java +++ b/src/main/java/spring/backend/member/application/ReadMemberHomeService.java @@ -9,7 +9,7 @@ import spring.backend.activity.query.dao.ActivityDao; import spring.backend.quickstart.query.dao.QuickStartDao; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.presentation.dto.response.HomeMainResponse; import spring.backend.member.dto.response.HomeMemberInfoResponse; import java.time.LocalDateTime; diff --git a/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java b/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java index 65842341f..f56210d9c 100644 --- a/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java +++ b/src/main/java/spring/backend/member/domain/service/CreateMemberWithOAuthService.java @@ -6,7 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; -import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; +import spring.backend.member.presentation.dto.request.CreateMemberWithOAuthRequest; import spring.backend.member.exception.MemberErrorCode; import java.util.List; diff --git a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java index 4c06f9771..9e37b7298 100644 --- a/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java +++ b/src/main/java/spring/backend/member/domain/service/EditMemberProfileService.java @@ -6,7 +6,7 @@ import org.springframework.transaction.annotation.Transactional; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; -import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.presentation.dto.request.EditMemberProfileRequest; @Service @RequiredArgsConstructor diff --git a/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java index 89807571e..fd7f9ce43 100644 --- a/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java +++ b/src/main/java/spring/backend/member/presentation/EditMemberProfileController.java @@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.presentation.dto.request.EditMemberProfileRequest; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.member.domain.service.EditMemberProfileService; diff --git a/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java index 4594f0982..c9aaed924 100644 --- a/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java +++ b/src/main/java/spring/backend/member/presentation/ReadMemberHomeController.java @@ -9,7 +9,7 @@ import spring.backend.core.presentation.RestResponse; import spring.backend.member.application.ReadMemberHomeService; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.presentation.dto.response.HomeMainResponse; import spring.backend.member.presentation.swagger.ReadMemberHomeSwagger; @RestController diff --git a/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java b/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java index 37bd779a4..a281c4ffd 100644 --- a/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java +++ b/src/main/java/spring/backend/member/presentation/ReadMemberProfileController.java @@ -8,7 +8,7 @@ import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.response.MemberProfileResponse; +import spring.backend.member.presentation.dto.response.MemberProfileResponse; import spring.backend.member.presentation.swagger.ReadMemberProfileSwagger; @RestController diff --git a/src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java b/src/main/java/spring/backend/member/presentation/dto/request/CreateMemberWithOAuthRequest.java similarity index 82% rename from src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java rename to src/main/java/spring/backend/member/presentation/dto/request/CreateMemberWithOAuthRequest.java index f8d88f263..6d3fc78d2 100644 --- a/src/main/java/spring/backend/member/dto/request/CreateMemberWithOAuthRequest.java +++ b/src/main/java/spring/backend/member/presentation/dto/request/CreateMemberWithOAuthRequest.java @@ -1,4 +1,4 @@ -package spring.backend.member.dto.request; +package spring.backend.member.presentation.dto.request; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java b/src/main/java/spring/backend/member/presentation/dto/request/EditMemberProfileRequest.java similarity index 92% rename from src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java rename to src/main/java/spring/backend/member/presentation/dto/request/EditMemberProfileRequest.java index 3fb736200..e5b6313d3 100644 --- a/src/main/java/spring/backend/member/dto/request/EditMemberProfileRequest.java +++ b/src/main/java/spring/backend/member/presentation/dto/request/EditMemberProfileRequest.java @@ -1,4 +1,4 @@ -package spring.backend.member.dto.request; +package spring.backend.member.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java b/src/main/java/spring/backend/member/presentation/dto/response/HomeMainResponse.java similarity index 87% rename from src/main/java/spring/backend/member/dto/response/HomeMainResponse.java rename to src/main/java/spring/backend/member/presentation/dto/response/HomeMainResponse.java index c0e8e1993..cea691104 100644 --- a/src/main/java/spring/backend/member/dto/response/HomeMainResponse.java +++ b/src/main/java/spring/backend/member/presentation/dto/response/HomeMainResponse.java @@ -1,7 +1,8 @@ -package spring.backend.member.dto.response; +package spring.backend.member.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.dto.response.HomeActivityInfoResponse; +import spring.backend.member.dto.response.HomeMemberInfoResponse; import spring.backend.quickstart.dto.response.QuickStartResponse; import java.util.List; diff --git a/src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java b/src/main/java/spring/backend/member/presentation/dto/response/MemberProfileResponse.java similarity index 90% rename from src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java rename to src/main/java/spring/backend/member/presentation/dto/response/MemberProfileResponse.java index aabe83431..790d36054 100644 --- a/src/main/java/spring/backend/member/dto/response/MemberProfileResponse.java +++ b/src/main/java/spring/backend/member/presentation/dto/response/MemberProfileResponse.java @@ -1,4 +1,4 @@ -package spring.backend.member.dto.response; +package spring.backend.member.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java index d091b00f2..f883c9e3b 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/EditMemberProfileSwagger.java @@ -6,7 +6,7 @@ import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.request.EditMemberProfileRequest; +import spring.backend.member.presentation.dto.request.EditMemberProfileRequest; import spring.backend.member.exception.MemberErrorCode; @Tag(name = "Member", description = "멤버") diff --git a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java index 50889512b..0c14e1278 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberHomeSwagger.java @@ -8,7 +8,7 @@ import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.response.HomeMainResponse; +import spring.backend.member.presentation.dto.response.HomeMainResponse; import spring.backend.member.exception.MemberErrorCode; @Tag(name = "Member", description = "멤버") diff --git a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java index f6053792c..de0bcc0ed 100644 --- a/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java +++ b/src/main/java/spring/backend/member/presentation/swagger/ReadMemberProfileSwagger.java @@ -8,7 +8,7 @@ import spring.backend.core.exception.error.GlobalErrorCode; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; -import spring.backend.member.dto.response.MemberProfileResponse; +import spring.backend.member.presentation.dto.response.MemberProfileResponse; @Tag(name = "Member", description = "멤버") public interface ReadMemberProfileSwagger { diff --git a/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java b/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java index 09f7c8ec1..c1d0e1f8b 100644 --- a/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java +++ b/src/test/java/spring/backend/member/domain/service/CreateMemberWithOAuthServiceTest.java @@ -10,7 +10,7 @@ import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.domain.value.Provider; import spring.backend.member.domain.value.Role; -import spring.backend.member.dto.request.CreateMemberWithOAuthRequest; +import spring.backend.member.presentation.dto.request.CreateMemberWithOAuthRequest; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; diff --git a/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java b/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java index 1addca58b..e7125fe5d 100644 --- a/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java +++ b/src/test/java/spring/backend/member/dto/response/MemberProfileResponseTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import spring.backend.member.domain.entity.Member; +import spring.backend.member.presentation.dto.response.MemberProfileResponse; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; From 7b3faa1c53783d5d5c5e0b53ff10a4ee91299fff Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 21:14:03 +0900 Subject: [PATCH 415/478] =?UTF-8?q?refactor:=20(#170)=20Auth=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=98=20dto=20=EA=B5=AC=EC=A1=B0=EB=A5=BC?= =?UTF-8?q?=20controller=20=EC=82=AC=EC=9A=A9=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/QuickStartActivitySelectController.java | 1 - .../ReadActivitiesByMemberAndKeywordInMonthController.java | 1 - .../presentation/ReadMonthlyActivityOverviewController.java | 1 - .../ReadActivitiesByMemberAndKeywordInMonthSwagger.java | 2 -- .../backend/auth/application/HandleOAuthLoginService.java | 6 +++--- .../backend/auth/application/OnboardingSignUpService.java | 4 ++-- .../backend/auth/application/RotateAccessTokenService.java | 2 +- .../spring/backend/auth/infrastructure/OAuthRestClient.java | 4 ++-- .../auth/infrastructure/google/GoogleOAuthRestClient.java | 4 ++-- .../auth/infrastructure/kakao/KakaoOAuthRestClient.java | 4 ++-- .../auth/infrastructure/naver/NaverOAuthRestClient.java | 4 ++-- .../auth/presentation/HandleOAuthLoginController.java | 2 +- .../auth/presentation/OnboardingSignUpController.java | 4 ++-- .../auth/presentation/RotateAccessTokenController.java | 2 +- .../dto/request/OnboardingSignUpRequest.java | 2 +- .../auth/{ => presentation}/dto/response/LoginResponse.java | 2 +- .../dto/response/OAuthAccessTokenResponse.java | 2 +- .../dto/response/OAuthResourceResponse.java | 2 +- .../dto/response/OnboardingSignUpResponse.java | 2 +- .../dto/response/RotateAccessTokenResponse.java | 2 +- .../auth/presentation/swagger/OnboardingSignUpSwagger.java | 4 ++-- .../auth/application/OnboardingSignUpServiceTest.java | 4 ++-- 22 files changed, 28 insertions(+), 33 deletions(-) rename src/main/java/spring/backend/auth/{ => presentation}/dto/request/OnboardingSignUpRequest.java (94%) rename src/main/java/spring/backend/auth/{ => presentation}/dto/response/LoginResponse.java (97%) rename src/main/java/spring/backend/auth/{ => presentation}/dto/response/OAuthAccessTokenResponse.java (87%) rename src/main/java/spring/backend/auth/{ => presentation}/dto/response/OAuthResourceResponse.java (84%) rename src/main/java/spring/backend/auth/{ => presentation}/dto/response/OnboardingSignUpResponse.java (95%) rename src/main/java/spring/backend/auth/{ => presentation}/dto/response/RotateAccessTokenResponse.java (54%) diff --git a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java index cc44dd2ee..139405162 100644 --- a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java @@ -12,7 +12,6 @@ import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.presentation.swagger.QuickStartActivitySelectSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; -import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java index c8979783d..06761a66d 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -3,7 +3,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java index 3d1c2af31..ef7c53a8a 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadMonthlyActivityOverviewService; import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java index c0fa93442..3cb303983 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java @@ -2,10 +2,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.core.presentation.RestResponse; diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 8e69cfd38..0536a09df 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -3,9 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; -import spring.backend.auth.dto.response.LoginResponse; -import spring.backend.auth.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.presentation.dto.response.LoginResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.OAuthRestClientFactory; diff --git a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java index 34d511a26..08849d5df 100644 --- a/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java +++ b/src/main/java/spring/backend/auth/application/OnboardingSignUpService.java @@ -4,8 +4,8 @@ import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import spring.backend.auth.dto.request.OnboardingSignUpRequest; -import spring.backend.auth.dto.response.OnboardingSignUpResponse; +import spring.backend.auth.presentation.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.presentation.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index 769ad2b02..d2e518323 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import spring.backend.auth.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java index 9e7aa3b92..113f4ff31 100644 --- a/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/OAuthRestClient.java @@ -1,7 +1,7 @@ package spring.backend.auth.infrastructure; -import spring.backend.auth.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import java.net.URI; diff --git a/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java index 78392823b..c8a395c44 100644 --- a/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/google/GoogleOAuthRestClient.java @@ -8,8 +8,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; -import spring.backend.auth.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.core.configuration.property.oauth.GoogleOAuthProperty; diff --git a/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java index ff91dd14b..55997800a 100644 --- a/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/kakao/KakaoOAuthRestClient.java @@ -8,8 +8,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; -import spring.backend.auth.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.kakao.dto.KakaoResourceResponse; diff --git a/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java index 1aa95f285..c5a1cb535 100644 --- a/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java +++ b/src/main/java/spring/backend/auth/infrastructure/naver/NaverOAuthRestClient.java @@ -8,8 +8,8 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; -import spring.backend.auth.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.dto.response.OAuthResourceResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.naver.dto.NaverResourceResponse; diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index 936dfbe38..54544d16c 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -4,7 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import spring.backend.auth.application.HandleOAuthLoginService; -import spring.backend.auth.dto.response.LoginResponse; +import spring.backend.auth.presentation.dto.response.LoginResponse; import spring.backend.core.presentation.RestResponse; @RestController diff --git a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java index 73083bbe8..3dca0039e 100644 --- a/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java +++ b/src/main/java/spring/backend/auth/presentation/OnboardingSignUpController.java @@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.OnboardingSignUpService; -import spring.backend.auth.dto.request.OnboardingSignUpRequest; -import spring.backend.auth.dto.response.OnboardingSignUpResponse; +import spring.backend.auth.presentation.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.presentation.dto.response.OnboardingSignUpResponse; import spring.backend.auth.presentation.swagger.OnboardingSignUpSwagger; import spring.backend.core.configuration.argumentresolver.LoginMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 600914b38..86b2b47df 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; -import spring.backend.auth.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.core.presentation.RestResponse; @RestController diff --git a/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java b/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java similarity index 94% rename from src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java rename to src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java index 033f2535b..03648b55c 100644 --- a/src/main/java/spring/backend/auth/dto/request/OnboardingSignUpRequest.java +++ b/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.request; +package spring.backend.auth.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/LoginResponse.java similarity index 97% rename from src/main/java/spring/backend/auth/dto/response/LoginResponse.java rename to src/main/java/spring/backend/auth/presentation/dto/response/LoginResponse.java index f165da425..f84c17f0e 100644 --- a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java +++ b/src/main/java/spring/backend/auth/presentation/dto/response/LoginResponse.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.response; +package spring.backend.auth.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/OAuthAccessTokenResponse.java similarity index 87% rename from src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java rename to src/main/java/spring/backend/auth/presentation/dto/response/OAuthAccessTokenResponse.java index 07146cc02..b6327a6f3 100644 --- a/src/main/java/spring/backend/auth/dto/response/OAuthAccessTokenResponse.java +++ b/src/main/java/spring/backend/auth/presentation/dto/response/OAuthAccessTokenResponse.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.response; +package spring.backend.auth.presentation.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/OAuthResourceResponse.java similarity index 84% rename from src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java rename to src/main/java/spring/backend/auth/presentation/dto/response/OAuthResourceResponse.java index 1cb2bf3ae..2da417378 100644 --- a/src/main/java/spring/backend/auth/dto/response/OAuthResourceResponse.java +++ b/src/main/java/spring/backend/auth/presentation/dto/response/OAuthResourceResponse.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.response; +package spring.backend.auth.presentation.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; diff --git a/src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/OnboardingSignUpResponse.java similarity index 95% rename from src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java rename to src/main/java/spring/backend/auth/presentation/dto/response/OnboardingSignUpResponse.java index dff23c107..490b9d43d 100644 --- a/src/main/java/spring/backend/auth/dto/response/OnboardingSignUpResponse.java +++ b/src/main/java/spring/backend/auth/presentation/dto/response/OnboardingSignUpResponse.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.response; +package spring.backend.auth.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.member.domain.value.Gender; diff --git a/src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java similarity index 54% rename from src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java rename to src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java index 949638034..0cd8755a7 100644 --- a/src/main/java/spring/backend/auth/dto/response/RotateAccessTokenResponse.java +++ b/src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java @@ -1,4 +1,4 @@ -package spring.backend.auth.dto.response; +package spring.backend.auth.presentation.dto.response; public record RotateAccessTokenResponse(String accessToken) { } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java index 03c4cf40a..0e5138c62 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/OnboardingSignUpSwagger.java @@ -4,8 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.auth.dto.request.OnboardingSignUpRequest; -import spring.backend.auth.dto.response.OnboardingSignUpResponse; +import spring.backend.auth.presentation.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.presentation.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java index 7ef70d723..cd3145136 100644 --- a/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java +++ b/src/test/java/spring/backend/auth/application/OnboardingSignUpServiceTest.java @@ -6,8 +6,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import spring.backend.auth.dto.request.OnboardingSignUpRequest; -import spring.backend.auth.dto.response.OnboardingSignUpResponse; +import spring.backend.auth.presentation.dto.request.OnboardingSignUpRequest; +import spring.backend.auth.presentation.dto.response.OnboardingSignUpResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; From 05f2ac1ac4933545930164f8bd5d6f34043cf95c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 28 Nov 2024 21:30:52 +0900 Subject: [PATCH 416/478] =?UTF-8?q?refactor:=20(#170)=20Activity=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=98=20dto=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20controller=20=EC=82=AC=EC=9A=A9=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/QuickStartActivitySelectService.java | 4 ++-- .../ReadActivitiesByMemberAndKeywordInMonthService.java | 2 +- .../application/ReadMonthlyActivityOverviewService.java | 4 ++-- .../activity/application/UserActivitySelectService.java | 4 ++-- .../activity/domain/service/FinishActivityService.java | 2 +- .../persistence/jpa/dao/ActivityJpaDao.java | 8 ++++---- .../activity/presentation/FinishActivityController.java | 2 +- .../presentation/QuickStartActivitySelectController.java | 4 ++-- ...ReadActivitiesByMemberAndKeywordInMonthController.java | 4 ++-- .../presentation/ReadActivityCalendarController.java | 8 ++++---- .../ReadMonthlyActivityOverviewController.java | 4 ++-- .../presentation/UserActivitySelectController.java | 4 ++-- .../ActivitiesByMemberAndKeywordInMonthRequest.java | 2 +- .../dto/request/MonthlyActivityOverviewRequest.java | 2 +- .../dto/request/QuickStartActivitySelectRequest.java | 2 +- .../dto/request/ReadActivityCalendarRequest.java | 2 +- .../dto/request/UserActivitySelectRequest.java | 2 +- .../ActivitiesByMemberAndKeywordInMonthResponse.java | 3 ++- .../dto/response/ActivityCalendarResponse.java | 2 +- .../dto/response/FinishActivityResponse.java | 3 ++- .../dto/response/MonthlyActivityOverviewResponse.java | 4 +++- .../dto/response/QuickStartActivitySelectResponse.java | 2 +- .../dto/response/UserActivitySelectResponse.java | 2 +- .../dto/response/UserMonthlyActivityDetail.java | 2 +- .../dto/response/UserMonthlyActivitySummary.java | 2 +- .../presentation/swagger/FinishActivitySwagger.java | 2 +- .../swagger/QuickStartActivitySelectSwagger.java | 4 ++-- .../ReadActivitiesByMemberAndKeywordInMonthSwagger.java | 4 ++-- .../presentation/swagger/ReadActivityCalendarSwagger.java | 4 ++-- .../swagger/ReadMonthlyActivityOverviewSwagger.java | 4 ++-- .../presentation/swagger/UserActivitySelectSwagger.java | 4 ++-- .../spring/backend/activity/query/dao/ActivityDao.java | 4 ++-- .../application/QuickStartActivitySelectServiceTest.java | 4 ++-- .../application/UserActivitySelectServiceTest.java | 4 ++-- .../domain/repository/ActivityRepositoryTest.java | 4 ++-- 35 files changed, 61 insertions(+), 57 deletions(-) rename src/main/java/spring/backend/activity/{ => presentation}/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java (94%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/request/MonthlyActivityOverviewRequest.java (88%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/request/QuickStartActivitySelectRequest.java (96%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/request/ReadActivityCalendarRequest.java (91%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/request/UserActivitySelectRequest.java (96%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java (85%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/ActivityCalendarResponse.java (94%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/FinishActivityResponse.java (73%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/MonthlyActivityOverviewResponse.java (75%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/QuickStartActivitySelectResponse.java (90%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/UserActivitySelectResponse.java (90%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/UserMonthlyActivityDetail.java (93%) rename src/main/java/spring/backend/activity/{ => presentation}/dto/response/UserMonthlyActivitySummary.java (90%) diff --git a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java index e8e3f700d..069d1de86 100644 --- a/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/QuickStartActivitySelectService.java @@ -8,8 +8,8 @@ import spring.backend.activity.domain.repository.ActivityRepository; import spring.backend.quickstart.domain.repository.QuickStartRepository; import spring.backend.activity.domain.service.FinishActivityAutoService; -import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; -import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java index a1049f43a..7f9055773 100644 --- a/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java +++ b/src/main/java/spring/backend/activity/application/ReadActivitiesByMemberAndKeywordInMonthService.java @@ -4,7 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.value.Keyword; -import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.activity.presentation.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; import spring.backend.activity.dto.response.TotalSavedTimeAndActivityCountByKeywordInMonth; import spring.backend.activity.query.dao.ActivityDao; diff --git a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java index f435914f7..55e92449e 100644 --- a/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java +++ b/src/main/java/spring/backend/activity/application/ReadMonthlyActivityOverviewService.java @@ -5,9 +5,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spring.backend.activity.domain.value.Keyword.Category; -import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.presentation.dto.request.MonthlyActivityOverviewRequest; import spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse; -import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.activity.presentation.dto.response.MonthlyActivityOverviewResponse; import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; import spring.backend.activity.infrastructure.persistence.jpa.value.KeywordJpaValue; import spring.backend.activity.query.dao.ActivityDao; diff --git a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java index 1980dbc13..b018220af 100644 --- a/src/main/java/spring/backend/activity/application/UserActivitySelectService.java +++ b/src/main/java/spring/backend/activity/application/UserActivitySelectService.java @@ -7,8 +7,8 @@ import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; import spring.backend.activity.domain.service.FinishActivityAutoService; -import spring.backend.activity.dto.request.UserActivitySelectRequest; -import spring.backend.activity.dto.response.UserActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.UserActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java b/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java index 5f88b136f..18ce23de5 100644 --- a/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java +++ b/src/main/java/spring/backend/activity/domain/service/FinishActivityService.java @@ -7,7 +7,7 @@ import spring.backend.activity.domain.entity.Activity; import spring.backend.activity.domain.repository.ActivityRepository; import spring.backend.activity.dto.response.ActivityInfo; -import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.presentation.dto.response.FinishActivityResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.member.domain.entity.Member; import spring.backend.member.dto.response.HomeMemberInfoResponse; diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index 85f928170..bfe84a2b0 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -5,8 +5,8 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; -import spring.backend.activity.dto.response.UserMonthlyActivityDetail; -import spring.backend.activity.dto.response.UserMonthlyActivitySummary; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivitySummary; import spring.backend.activity.infrastructure.persistence.jpa.entity.ActivityJpaEntity; import spring.backend.activity.query.dao.ActivityDao; @@ -62,7 +62,7 @@ order by count (a) desc @Override @Query(""" - select new spring.backend.activity.dto.response.UserMonthlyActivitySummary( + select new spring.backend.activity.presentation.dto.response.UserMonthlyActivitySummary( m.createdAt, coalesce(sum(a.savedTime), 0), count(a) @@ -79,7 +79,7 @@ and function('month', a.createdAt) = :month @Override @Query(""" - select new spring.backend.activity.dto.response.UserMonthlyActivityDetail( + select new spring.backend.activity.presentation.dto.response.UserMonthlyActivityDetail( a.keyword.category, a.title, a.savedTime, diff --git a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java index df6954a71..a4728d98e 100644 --- a/src/main/java/spring/backend/activity/presentation/FinishActivityController.java +++ b/src/main/java/spring/backend/activity/presentation/FinishActivityController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.domain.service.FinishActivityService; -import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.presentation.dto.response.FinishActivityResponse; import spring.backend.activity.presentation.swagger.FinishActivitySwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java index 139405162..bd5198f41 100644 --- a/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/QuickStartActivitySelectController.java @@ -8,8 +8,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.QuickStartActivitySelectService; -import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; -import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.presentation.swagger.QuickStartActivitySelectSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java index 06761a66d..02eb1a424 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivitiesByMemberAndKeywordInMonthController.java @@ -6,8 +6,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadActivitiesByMemberAndKeywordInMonthService; -import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; -import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.activity.presentation.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.presentation.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.activity.presentation.swagger.ReadActivitiesByMemberAndKeywordInMonthSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java b/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java index 830ba4d3a..b786b8b19 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadActivityCalendarController.java @@ -5,10 +5,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import spring.backend.activity.dto.request.ReadActivityCalendarRequest; -import spring.backend.activity.dto.response.ActivityCalendarResponse; -import spring.backend.activity.dto.response.UserMonthlyActivityDetail; -import spring.backend.activity.dto.response.UserMonthlyActivitySummary; +import spring.backend.activity.presentation.dto.request.ReadActivityCalendarRequest; +import spring.backend.activity.presentation.dto.response.ActivityCalendarResponse; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivitySummary; import spring.backend.activity.presentation.swagger.ReadActivityCalendarSwagger; import spring.backend.activity.query.dao.ActivityDao; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; diff --git a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java index ef7c53a8a..289340df6 100644 --- a/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java +++ b/src/main/java/spring/backend/activity/presentation/ReadMonthlyActivityOverviewController.java @@ -6,8 +6,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.ReadMonthlyActivityOverviewService; -import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; -import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.activity.presentation.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.presentation.dto.response.MonthlyActivityOverviewResponse; import spring.backend.activity.presentation.swagger.ReadMonthlyActivityOverviewSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java index 46c91ce82..028781c62 100644 --- a/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java +++ b/src/main/java/spring/backend/activity/presentation/UserActivitySelectController.java @@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import spring.backend.activity.application.UserActivitySelectService; -import spring.backend.activity.dto.request.UserActivitySelectRequest; -import spring.backend.activity.dto.response.UserActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.UserActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.UserActivitySelectResponse; import spring.backend.activity.presentation.swagger.UserActivitySelectSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; diff --git a/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java b/src/main/java/spring/backend/activity/presentation/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java similarity index 94% rename from src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java rename to src/main/java/spring/backend/activity/presentation/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java index cf6e074e2..931a8e7a2 100644 --- a/src/main/java/spring/backend/activity/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java +++ b/src/main/java/spring/backend/activity/presentation/dto/request/ActivitiesByMemberAndKeywordInMonthRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.activity.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; diff --git a/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java b/src/main/java/spring/backend/activity/presentation/dto/request/MonthlyActivityOverviewRequest.java similarity index 88% rename from src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java rename to src/main/java/spring/backend/activity/presentation/dto/request/MonthlyActivityOverviewRequest.java index 3770f0dcb..902e0803c 100644 --- a/src/main/java/spring/backend/activity/dto/request/MonthlyActivityOverviewRequest.java +++ b/src/main/java/spring/backend/activity/presentation/dto/request/MonthlyActivityOverviewRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.activity.presentation.dto.request; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; diff --git a/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java b/src/main/java/spring/backend/activity/presentation/dto/request/QuickStartActivitySelectRequest.java similarity index 96% rename from src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java rename to src/main/java/spring/backend/activity/presentation/dto/request/QuickStartActivitySelectRequest.java index feaa609f5..d38de3560 100644 --- a/src/main/java/spring/backend/activity/dto/request/QuickStartActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/presentation/dto/request/QuickStartActivitySelectRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.activity.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; diff --git a/src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java b/src/main/java/spring/backend/activity/presentation/dto/request/ReadActivityCalendarRequest.java similarity index 91% rename from src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java rename to src/main/java/spring/backend/activity/presentation/dto/request/ReadActivityCalendarRequest.java index db6289cd9..4ec0026a6 100644 --- a/src/main/java/spring/backend/activity/dto/request/ReadActivityCalendarRequest.java +++ b/src/main/java/spring/backend/activity/presentation/dto/request/ReadActivityCalendarRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.activity.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; diff --git a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java b/src/main/java/spring/backend/activity/presentation/dto/request/UserActivitySelectRequest.java similarity index 96% rename from src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java rename to src/main/java/spring/backend/activity/presentation/dto/request/UserActivitySelectRequest.java index 43defb561..3e4829c54 100644 --- a/src/main/java/spring/backend/activity/dto/request/UserActivitySelectRequest.java +++ b/src/main/java/spring/backend/activity/presentation/dto/request/UserActivitySelectRequest.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.request; +package spring.backend.activity.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; diff --git a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java similarity index 85% rename from src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java index 468c6a6d0..f2ea209f9 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/ActivitiesByMemberAndKeywordInMonthResponse.java @@ -1,7 +1,8 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword; +import spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse; import java.util.List; diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/ActivityCalendarResponse.java similarity index 94% rename from src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/ActivityCalendarResponse.java index 036325fe8..83b98c6cc 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivityCalendarResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/ActivityCalendarResponse.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/FinishActivityResponse.java similarity index 73% rename from src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/FinishActivityResponse.java index d1f3e4d0d..d3a23a962 100644 --- a/src/main/java/spring/backend/activity/dto/response/FinishActivityResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/FinishActivityResponse.java @@ -1,6 +1,7 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.dto.response.ActivityInfo; import spring.backend.member.dto.response.HomeMemberInfoResponse; public record FinishActivityResponse( diff --git a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/MonthlyActivityOverviewResponse.java similarity index 75% rename from src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/MonthlyActivityOverviewResponse.java index 5c12ab878..e5bdd2974 100644 --- a/src/main/java/spring/backend/activity/dto/response/MonthlyActivityOverviewResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/MonthlyActivityOverviewResponse.java @@ -1,6 +1,8 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.activity.dto.response.MonthlyActivityCountByKeywordResponse; +import spring.backend.activity.dto.response.MonthlySavedTimeAndActivityCountResponse; import java.time.Month; import java.util.List; diff --git a/src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/QuickStartActivitySelectResponse.java similarity index 90% rename from src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/QuickStartActivitySelectResponse.java index 6044b7cf0..41c38d36e 100644 --- a/src/main/java/spring/backend/activity/dto/response/QuickStartActivitySelectResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/QuickStartActivitySelectResponse.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword; diff --git a/src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java b/src/main/java/spring/backend/activity/presentation/dto/response/UserActivitySelectResponse.java similarity index 90% rename from src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java rename to src/main/java/spring/backend/activity/presentation/dto/response/UserActivitySelectResponse.java index dec9121d0..e1d09e8cf 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserActivitySelectResponse.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/UserActivitySelectResponse.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword; diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java b/src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivityDetail.java similarity index 93% rename from src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java rename to src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivityDetail.java index 354027e76..60df3d07e 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivityDetail.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivityDetail.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import spring.backend.activity.domain.value.Keyword.Category; diff --git a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java b/src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivitySummary.java similarity index 90% rename from src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java rename to src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivitySummary.java index fa7a0810f..c3e5d08d7 100644 --- a/src/main/java/spring/backend/activity/dto/response/UserMonthlyActivitySummary.java +++ b/src/main/java/spring/backend/activity/presentation/dto/response/UserMonthlyActivitySummary.java @@ -1,4 +1,4 @@ -package spring.backend.activity.dto.response; +package spring.backend.activity.presentation.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java index b345d519a..e2ec09c61 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/FinishActivitySwagger.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.response.FinishActivityResponse; +import spring.backend.activity.presentation.dto.response.FinishActivityResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java index 24580f702..4db151932 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/QuickStartActivitySelectSwagger.java @@ -4,8 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; -import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java index 3cb303983..18beb784e 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivitiesByMemberAndKeywordInMonthSwagger.java @@ -4,8 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; -import spring.backend.activity.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; +import spring.backend.activity.presentation.dto.request.ActivitiesByMemberAndKeywordInMonthRequest; +import spring.backend.activity.presentation.dto.response.ActivitiesByMemberAndKeywordInMonthResponse; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java index 03166ac95..476d4b387 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadActivityCalendarSwagger.java @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.ReadActivityCalendarRequest; -import spring.backend.activity.dto.response.ActivityCalendarResponse; +import spring.backend.activity.presentation.dto.request.ReadActivityCalendarRequest; +import spring.backend.activity.presentation.dto.response.ActivityCalendarResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java index ad997a87f..633937659 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/ReadMonthlyActivityOverviewSwagger.java @@ -4,8 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.MonthlyActivityOverviewRequest; -import spring.backend.activity.dto.response.MonthlyActivityOverviewResponse; +import spring.backend.activity.presentation.dto.request.MonthlyActivityOverviewRequest; +import spring.backend.activity.presentation.dto.response.MonthlyActivityOverviewResponse; import spring.backend.core.presentation.RestResponse; import spring.backend.member.domain.entity.Member; diff --git a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java index 7c070f871..fe9455b84 100644 --- a/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java +++ b/src/main/java/spring/backend/activity/presentation/swagger/UserActivitySelectSwagger.java @@ -4,8 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.activity.dto.request.UserActivitySelectRequest; -import spring.backend.activity.dto.response.UserActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.UserActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; diff --git a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java index 5fbf99fb5..8eb2c52d8 100644 --- a/src/main/java/spring/backend/activity/query/dao/ActivityDao.java +++ b/src/main/java/spring/backend/activity/query/dao/ActivityDao.java @@ -3,8 +3,8 @@ import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.dto.response.*; import spring.backend.activity.dto.response.HomeActivityInfoResponse; -import spring.backend.activity.dto.response.UserMonthlyActivityDetail; -import spring.backend.activity.dto.response.UserMonthlyActivitySummary; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivityDetail; +import spring.backend.activity.presentation.dto.response.UserMonthlyActivitySummary; import java.time.LocalDateTime; import java.util.List; diff --git a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java index decdb1a0a..f665cd99a 100644 --- a/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/QuickStartActivitySelectServiceTest.java @@ -14,8 +14,8 @@ import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -import spring.backend.activity.dto.request.QuickStartActivitySelectRequest; -import spring.backend.activity.dto.response.QuickStartActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.QuickStartActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.QuickStartActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.quickstart.exception.QuickStartErrorCode; import spring.backend.core.exception.DomainException; diff --git a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java index 9d5747e0b..f0a767fb4 100644 --- a/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java +++ b/src/test/java/spring/backend/activity/application/UserActivitySelectServiceTest.java @@ -12,8 +12,8 @@ import spring.backend.activity.domain.service.FinishActivityAutoService; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Type; -import spring.backend.activity.dto.request.UserActivitySelectRequest; -import spring.backend.activity.dto.response.UserActivitySelectResponse; +import spring.backend.activity.presentation.dto.request.UserActivitySelectRequest; +import spring.backend.activity.presentation.dto.response.UserActivitySelectResponse; import spring.backend.activity.exception.ActivityErrorCode; import spring.backend.core.exception.DomainException; import spring.backend.member.domain.entity.Member; diff --git a/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java index 03e8e9c94..e1b5e21db 100644 --- a/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java +++ b/src/test/java/spring/backend/activity/domain/repository/ActivityRepositoryTest.java @@ -11,7 +11,7 @@ import java.time.LocalDateTime; import java.util.UUID; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @SpringBootTest class ActivityRepositoryTest { @@ -49,4 +49,4 @@ void testSaveAndFindActivity() { assertThat(foundActivity).isNotNull(); assertThat(foundActivity.getKeyword()).isEqualTo(activity.getKeyword()); } -} \ No newline at end of file +} From 6795de5ca2b80293c7f14fc5f9bcc42d01ef7204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=B1=EB=AF=BC?= <123073840+anxi01@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:30:28 +0900 Subject: [PATCH 417/478] =?UTF-8?q?docs:=20README=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 532 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..22e5be2b0 --- /dev/null +++ b/README.md @@ -0,0 +1,532 @@ +![intro](https://github.com/user-attachments/assets/5498e204-658e-40b8-9a3e-47b7f1bf643a) +# ⭐ 팀 소개 + +> **서비스명** : **조각조각** ⏰ + +‘조각조각’은 자투리 시간이라는 ’작은 조각‘들을 모아 하나의 큰 퍼즐인 ‘나만의 시간‘을 만들어낸다는 의미를 담았습니다. 특히, 시계를 표현하는 의성어인 ’째깍째깍‘과 유사한 초성을 이용하여, ’시간‘이라는 컨셉에 더욱 충실할 수 있도록 했습니다. + +> **팀명** : **C-nergy 시너지** 💛 + +C팀 + energy의 합성어로, C팀의 모든 팀원들이 각자의 에너지를 가지고 함께 함으로써 결국 최고의 시너지를 만들어가겠다는 의미를 담았습니다 😊 + + +> **R&R분배** + +| **분야** | **이름** | **포지션** | +| --- | --- | --- | +| ⚡️기획 | 한나영 | PM, 서비스 기획 - 데스크 리서치, 유저리서치, 아이데이션, 와이어프레임, 기능명세서, 화면명세서 | +| ⚡️기획 | 신예진 | 서비스 기획 - 데스크 리서치, 유저 리서치, 아이데이션, 와이어프레임, 페르소나 및 CJM, 화면명세서 | +| ⚡️기획 | 홍가연 | 서비스 기획 - 기획 리드, 데스크 리서치, 유저 리서치, 아이데이션, 와이어프레임, 기능명세서, 화면명세서 | +| 🎨디자인 | 최은정 | 프로덕트 디자인 - 브랜딩디자인, UXUI디자인, 그래픽디자인 | +| 💻프론트엔드 | 공예영 | UI 및 기능 구현, PR CI 구축 | +| 💻프론트엔드 | 황동준 | 프론트엔드 리드, UI 및 기능 구현, ncp 배포 및 CI/CD 구축 | +| 💻백엔드 | 전호영 | DB 및 API 구축, 서버 배포 ,네이버 클로바 추천 시스템 구현, 인프라 구축 | +| 💻백엔드 | 한성민 | 개발 리드, DB 설계 및 API 구현, OpenAI 추천 시스템 구현, 서버 배포 | + +# 🌟 서비스 개요 + +> **서비스 소개** + +⏰ ‘조각조각’은 **일상 속에서 발생하는 자투리 시간을 의미 있게 활용할 수 있도록 돕는 개인 맞춤형 활동 추천 서비스**입니다. + + +**`세부 기능`** + +- 사용자의 자투리 시간과 상황(온·오프라인 여부, 위치 등)을 기반으로 다양한 카테고리(자기개발/엔터테인먼크/휴식/문화예술/건강/소셜)별 활동 추천 리스트를 제공합니다. +- 사용자가 활용한 자투리 시간은 ‘시간 조각’으로 시각화해 아카이빙할 수 있으며, 자투리 시간을 온전한 ‘내 시간’으로 만들고 그 효능감을 인식할 수 있도록 돕습니다. + + +> **서비스 차별성** +> + +‘조각조각’은 일상 속에서 **불규칙하고 갑작스럽게 발생해 버려지기 쉬운 ‘자투리 시간’을 유의미한 활동으로 전환할 수 있도록** 돕습니다. 사용자의 시간 활용을 지원하는 일정 관리/루틴 관리 서비스와 같이 단순 생산성 향상과 시간 관리 가이드라인 제공을 목표로 하지 않는다는 점에서 차별화됩니다. + +특히 추천 카테고리의 다양화와 사용자 아카이빙 기록 시각화를 통해 재미 요소를 추가했으며, AI 기반의 추천 엔진(OpenAI, Clova AI)을 활용해 온/오프라인별 확장된 사용자 맞춤형 콘텐츠를 제공할 수 있습니다. + + +> **목표 사용자** +> + +‘조각조각’의 주요 목표 사용자는 **자투리 시간이 자주 발생하고, 시간을 의미있게 또는 다양한 방식으로 활용하고자 하는 니즈**가 있는 사람입니다. + +**`주요 타겟`** + +- 출퇴근 시간이나 학업 중 자투리 시간이 빈번히 발생하는 사람 +- 시간의 효율적인 활용 및 성취감을 중시하는 자기개발 니즈가 강한 사람 +- 반복적인 일상에서 벗어나 색다른/즐거운 활동을 경험하고 싶은 사람 + +# 🤔 문제 인식(Problem) + +## 서비스 개발 동기 + +‘하루 중에 버려지는 시간이 너무 많다 …’ 이런 생각 드신 적 있으신가요? + +배차 간격이 긴 버스를 기다릴 때, 갑자기 수업이 휴강 되었을 때, 약속 시간에 일찍 도착했을 때 … 무언가 새롭게 하기엔 애매한 것 같고, 멍하니 보내기엔 왠지 아까운 그 시간! + +>💡 팀 시너지는 우리의 일상에서 **누구에게나 발생할 수 있는 자투리 시간의 활용 한계와 아쉬움에 주목해**, +> **모든 사람들이 자신만의 의미있는 자투리 시간을 만들 수 있는 방법**을 찾아 ‘조각조각’ 서비스를 고안하게 되었습니다. + + +## 서비스 목적 및 필요성 + +‘조각조각’은 자투리 시간을 가볍지만 의미 있게 활용할 수 있는 방법을 제안해, **자투리 시간 활용에 대한 니즈와 페인포인트를 충족**합니다. +- 사용자의 시간과 상황, 취향을 반영한 개인화된 추천으로 `자투리 시간`을 `온전한 ‘내 시간'`으로 만들 수 있도록 돕습니다. +- 사용자가 자투리 시간에 대한 인식을 `버려지는 시간`, `애매한 시간`이 아닌 `’무엇이든 가볍게 시도해볼 수 있는 시간’`으로 전환할 수 있도록 만듭니다. + +> **1️⃣ 시간의 효율적 활용에 대한 니즈 확산** + +💡 **시간을 효율적으로, 잘 활용할 수 있는 방법을 제안하는 시의성 있는 서비스** + +시간의 가치를 중요하게 생각하는 사회적 동향과 함께, 24시간을 효율적으로 쓰고 싶어하는 니즈가 사회적으로 확산 되고 있습니다. + +- 2024 트렌드 코리아 키워드로도 선정된 ‘분초사회’는 시간이 중요성이 매우 강조되는 사회로, 시간의 효율성을 극대화하기 위해 분초(分秒)를 다투며 산다는 의미입니다. 이처럼 현대사회에서 시간의 중요성이 높아지면서 ‘시성비’라는 용어도 등장할 만큼 시간의 ‘가성비’를 중시하는 현상이 일어나고 있습니다. +- CJ ENM의 디지털마케팅 기업 메조미디어의 조사에 따르면 현대사회 사람들은 ‘현대사회에서 시간은 가장 큰 자원(82%)’이라고 생각하며 ‘남들보다 24시간을 효율적으로 써야 나의 가치를 더 높일 수 있다’고 답했습니다. **이는 시간의 가치를 무엇보다 중요하게 생각한다는 결과로, 시간을 효율적으로 사용하고자 하는 니즈가 사회적으로 퍼져있음을 확인할 수 있습니다.** + +출처: [분초사회](https://terms.naver.com/entry.naver?docId=6712649&cid=43667&categoryId=43667), [이명진 가자 내 시간은 '가장 큰 자원'... 시간 아끼는 '초단축 소비' 트렌드 대세](https://www.banronbodo.com/news/articleView.html?idxno=23135) + +> **2️⃣ 일상에서 빈번히 발생하는 자투리 시간에 대한 아쉬움** + +💡**매일 의미없이 버려지는 자투리 시간에 대한 아쉬움과 피로감을 해결할 수 있는 서비스** + + +우리의 24시간 중 자투리 시간은 얼마나 발생하고 있을까요? + +2019년 취업사이트 게임잡 설문 결과, 성인남녀의 자투리 시간은 **하루 평균 2시간 반(147분)** 에 달한 것으로 나타났습니다. 직장인은 하루 평균 127분, 대학생은 160분의 자투리 시간이 발생하며, **2명 중 1명(44.9%)은 '매일 발생하는 자투리 시간이 아깝다'** 고 답변했습니다. + +자체적으로 진행한 유저리서치에서도 20대 성인남녀가 **자투리 시간이 ‘버려지는 시간’ 또는 ‘무언가를 하기엔 짧고 애매한 시간’으로 여겨지며, 그 시간이 아깝다고 느낀다**고 답변했습니다. 특히 직장인의 경우, 출퇴근 과정에서 발생하는 **자투리 시간의 반복성과 긴 시간에 불편함**을 표하고 있었습니다. + +- '자투리시간 사용 행태 및 불편 사항'에 관한 설문조사 결과, 20대 설문 응답자가 자투리 시간 발생에 대해 약 80%가 불편함을 느끼고 있다고 답변했으며 그에 대한 이유는 버려지는 시간의 아까움(71.6%), 짧고 애매한 시간으로 무엇을 할지 고민 됨(58.1%)이 상위 응답으로 나타났습니다. +- 경기·인천 통학·통근 인구 153만 명 중 90% 이상(141만 명)이 서울로 이동하고 있으며(2020년 인구주택총조사), 직장인들이 출퇴근을 위해 하루에 소요하는 시간은 평균 1시간 24분으로 밝혀졌습니다(2022년 잡코리아 조사). 이러한 직장인들에게 출퇴근 길에 느끼는 피로도를 점수로 환산(*100점 만점 기준)하게 한 결과, 경기권은 74점, 서울과 지방은 71점을 보이며 직장인의 출퇴근 피로도의 심각성을 확인할 수 있었습니다. 이들에게 피로감을 느끼는 이유에 대해 복수응답으로 꼽아보게 한 결과로는 “오늘도 어김없이 출근이라는 현실 때문에 스트레스를 받는다”는 의견이 63%로 가장 높은 비율을 차지함이 드러났습니다. + +출처: [메트로신문 성인남녀 '하루 2시간여 자투리 시간'에 하는 것 톱5… 10명 중 8명은 '온라인 활동'](https://www.metroseoul.co.kr/article/2019102900083), [윤화정 기자 직장인 출퇴근 소요시간 평균 '1시간 24분'](http://www.worktoday.co.kr/news/articleView.html?idxno=26284) + +> **3️⃣ 반복되는 일상 속 새로운 활동에 대한 니즈** + +💡**남는 시간을 성취감 있게, 재미있게! 사용자에게 가치 있는 시간을 만들어 주는 서비스** + +그렇다면, 사용자는 자투리 시간에 **어떤 활동**을 하고 싶을까요? + +유저리서치 결과, 20대 설문 응답자들은 자투리 시간을 의미있게 만들기 위해 중요하게 생각하는 가치로 **습관 형성, 성취감, 재미있는 경험, 새로운 학습/지식 습득**을 꼽았습니다. 시간을 가치 있게 사용하고 싶어하는 성향이 반영되어 성취감을 느낄 수 있는 활동을 선호하는 것으로 판단됩니다. 실제로 통학 및 출퇴근 시간을 활용해 학습 앱으로 공부하며 체계적으로 시간을 관리하고, 자신만의 학업 경쟁력을 높이는 ‘틈새 학습 앱’이 인기를 끈다는 점에서도 그 니즈를 확인할 수 있었습니다. + +한편, Z세대를 중심으로 **일상적 상황에서 벗어난 재미있는 경험을 추구**하는 경향도 나타나고 있습니다. 낭만을 쫓는 ‘굳이 데이’, ‘도파민’ 등의 용어가 활발하게 쓰이며 색다르고 다양한 삶의 경험을 영위하고자 하는 니즈가 확산되고 있습니다. + +- '자투리시간 사용 행태 및 불편 사항'에 관한 설문조사 결과, 20대 설문 응답자의 92% 이상이 ‘자투리 시간을 의미있게 보내고 싶다’고 답했으며, ‘자투리 시간을 의미있게 만들기 위해 중요하다고 생각하는 가치’에 대해 습관 형성(57.3%), 성취감(50.6%), 재미있는 경험(46.1%), 새로운 학습/지식 습득(37.1%) 순으로 응답했습니다. +- 프로통학러들 사이에서 통학시간에 틈새 학습 앱으로 공부하며 체계적으로 시간을 관리하고, 자신만의 학업 경쟁력을 높이는 학습법이 각광받고 있습니다. 이러한 흐름에 발맞춰 YBM인강, 똑똑보카, 오르조 등 교육업계 서비스에서도 하루 30분 이상 도로에서 시간을 소모하는 통학러(통학생)들을 위해 언제, 어디서나 편하게 자투리 시간을 활용해 공부할 수 있는 학습 앱을 속속 선보이고 있습니다. +- Z세대 사이에서 ‘도파민’이 큰 화두입니다. 도파민은 스트레스를 완화해주고 기쁨을 느끼게 하는 우리 몸에 필수적인 호르몬입니다. 이에 따라 몰입이나 성취를 통해 느리지만 자연스럽게 도파민을 얻는 방식이 최근 떠오르고 있습니다. 더불어 최근 Z세대 사이 ‘낭만’이라는 단어가 자주 사용됩니다. 도파민을 추구하며 ‘낭만’이라는 이름으로 긍정적으로 색다른 경험들을 쫓고 다양한 삶의 경험을 영위하고 있습니다. + +출처: [이준문 기자 ‘프로통학러’ 학습 경쟁력 높이는 ‘틈새 학습 앱’ 각광](http://m.newstap.co.kr/news/articleView.html?idxno=204208), [캐릿이 4년 간 분석한 Z세대의 새로운 특징 ‘MZBTI 3.0’](https://www.careet.net/1293) + +# 👥 사용자 **(User)** + +## 유저 리서치 + +### Survey + +> 데스크 리서치를 통한 문제 인식과 배경 조사를 기반으로 96명에게 ‘자투리 시간 활용 행태 및 불편함’에 대한 설문 조사를 진행하였습니다. + + +**1️⃣ 자투리 시간 발생 현황 및 사용 행태** + +사람들이 자투리 시간을 어떻게 활용하고 있는 지 알아보았습니다. + +![survey](https://github.com/user-attachments/assets/b3009b4e-74a1-41e8-b419-a557cb04dcec) +![survey-1](https://github.com/user-attachments/assets/dcc05c0f-0cfe-4acc-b77d-37ab047127f2) + +- 응답자의 85% 이상이 자투리 시간이 자주 발생한다고 답변했으며, 주로 대중교통을 이용하거나 주요 일정의 휴식 시간에서 발생. +- 압도적인 수치로 소셜 미디어 및 음악 감상/영상 시청이 높은 편이었으며 이 외에도 책, 뉴스, 게임 등을 통해 자투리 시간을 활용하고 있음. + - 해당 활동으로 자투리 시간을 활용하는 이유로는 간편함, 높은 접근성, 생산성, 습관적, 휴식 등을 꼽았으며 어떤 일을 하기 애매한 시간이라 흘려보낸다는 답변도 많았음. + +⇒ 자투리 시간이 **애매한 시간이라는 인식**으로, 대부분 **간편하고 접근성이 높은** 휴대폰을 통한 활동을 한다는 것을 확인할 수 있었음. + +**2️⃣ 자투리 시간에서 느끼는 불편함** + +자투리 시간 발생 시 어떤 불편함을 느끼는 지 알아보았습니다. + +![survey-2](https://github.com/user-attachments/assets/3e535aad-4c7e-4605-a1b2-6568b0aa2de8) + +- 자투리 시간 발생에 대해 약 80%가 불편함을 느끼고 있다고 응답 + - 이에 대한 이유로는 버려지는 시간의 아까움, 짧고 애매한 시간으로 무엇을 할 지 고민 됨, 과도한 핸드폰 사용량, 지루함 등을 이유로 꼽음 + +⇒ 대부분의 사람들은 버려지는 시간을 아까워하지만 ‘자투리 시간’이 애매하다는 인식으로 어떤 일을 해야 할 지 모르고 있음. + +**3️⃣ 자투리 시간 활용 시 주요 가치** + +자투리 시간 활용에서 가장 중요하게 느끼는 것이 무엇인지 알아보았습니다. + +![survey-3](https://github.com/user-attachments/assets/67f217ca-6d5b-465d-8c2b-73d1a82d57a8) + +- 응답자의 92% 이상이 자투리 시간을 의미있게 보내고 싶다는 니즈가 있음 +- 자투리 시간 활용에서 중요하게 생각하는 것은 습관 형성, 성취감, 재미있는 경험 등 + +### In-Depth Interview + +> 시행 기간: 2024.10.10 ~ 10.13 총 4일간 +> +> 대상자: 자투리 시간을 의미있게 보내고 싶은 응답자&자투리 시간 활용에 불편함을 느끼는 응답자 중 5명 + +In-Depth Interview를 진행하기에 앞서, 인터뷰이들의 자투리 행태를 보다 면밀히 파악하기 위해 +자투리 시간 활용에 대한 자가 기록 연구를 부탁했습니다. + +> 🖊️ **[자가 기록 연구 내용]** +> +> 자투리 시간이 발생할 때마다 해당 폼을 통해 활용 행태를 적도록 함. +> +> 유의미한 정보 수집을 위해 최소 하루 이상의 자가 기록을 진행할 수 있도록 하였으며, 구글폼의 주어진 양식을 통해 제출할 수 있도록 하여 사용자의 기록 편의성을 높일 수 있게 함. +> - **자투리 시간 사용 전 :** 자투리 시간이 발생한 상황 (발생 이유/현재 시각/장소/발생한 자투리 시간) +> - **자투리 시간 사용 후 :** 자투리 시간 사용 행태 (사용 방법/이유/감정) + +자가 기록 연구와 서베이 응답을 기반으로 각 인터뷰이 별로 30분 내외의 심층 인터뷰를 진행하였고, +유사한 응답을 묶어 Affinity Diagram을 진행하였습니다. + +Affinity Diagram + +1. 자투리 시간에 대한 인식 + ⇒ 대부분의 인터뷰이들은 자투리 시간을 ‘애매하게 남는 시간’, ‘아까운 시간’, ‘예상치 못하게 발생하는 시간’이라고 인식하고 있었음. 이로 인해 해당 시간에 무엇을 하면 좋을 지 모르겠다고 응답함. + +2. 자투리 시간을 보내는 행태 + ⇒ 유의미한 일을 하고 싶다고 생각하지만 막상 자투리 시간이 다가오면 습관적으로 핸드폰을 키거나 무의미하게 흘려보내게 된다고 답해주었음. + +3. 자투리 시간을 보내는 것에 대한 아쉬움 + ⇒ 모든 응답자가 하고자 하는 일을 못하고 의미없이 릴스, SNS 등 내가 ‘목적’으로 하지 않은 일을 하게 될 때 아쉬움, 나에 대한 부정적인 감정, 회의감 등을 느끼게 됨. + +4. 자투리 시간에 대한 니즈 + ⇒ 사용자마다 어떤 방식으로 자투리 시간을 보내고 싶은 지는 상이했음. 자기개발, 독서, 스트레칭, 일정 관리 등. 하고 싶은 행위는 모두 달랐지만 근본적으로 자신이 유의미하다고 여기는 가치 및 목적에 대한 행위를 통해 시간을 낭비하지 않고 알차게 보내기를 바람. + + +) 유튜브/릴스가 이미 재미와 흥미를 제공하고 있지만 이에 대한 행위에 부정적 감정을 느끼는 이유는 ‘목적'이 없는, 나의 선택이 들어가지 않은 알고리즘에 의한 콘텐츠 소비이기에 의미가 없음. + +5. 서비스 기능 관련 + ⇒ 자투리 시간을 활용할 수 있는 활동을 추천 받는다면 본인에게 그 행위가 얼마나 유의미하고 매력적으로 다가올 지 중요할 것 같다고 응답. 또한 ‘자투리 시간’인만큼 부담스럽고 제약이 있는 활동 보다는 가볍고 편안한 활동을 원한다고 응답함. + +## 서비스 목표 타겟 정의 + +> 앞선 유저 리서치를 바탕으로, 자투리 시간에 대한 인식과 사용 니즈에 기반하여 서비스 목표 타겟을 정의하였습니다. + +1) 자투리 시간이 ’무엇을 하기에는 애매하게 남는 시간‘이라는 인식 + + → 무엇을 할 지 모르고 그냥 습관적으로 흘려보내게 되는 사람들이 대부분. + + → 결국 나의 의지가 담기지 못한 채 ‘목적성’을 잃고 흘려보내기 때문에 유의미한 활용이 어려움. +2) ‘자투리 시간’인 만큼 부담스럽고 거창한 활동보다 가볍고 접근성이 높은 활동에 대한 니즈 존재 + + +> ❗**예상치 못하게 발생하는 자투리 시간을 무의미하게 흘려보내지 않고** +> **가볍고 다양한 활동을 통해 ‘나만의 시간’으로 만들어가고 싶은 사람들** + + + +## Persona & Journey Map + +> 유저리서치 내용 및 인사이트를 바탕으로 서비스를 사용하는 메인 페르소나를 도출하였습니다. +> + +![페르소나](https://github.com/user-attachments/assets/80358cd9-76a9-4a58-b7d0-26baef8925ee) + +![저니맵](https://github.com/user-attachments/assets/0447c2db-da73-4755-9c35-a16e189e5d58) + +## 👩‍💻 서비스(Service) + +### 서비스 카테고리 + +> AI 기반 자투리 시간 활용 방향 추천 플랫폼 + +### 타겟특화 포인트 + +> 👥 자투리 시간을 유의미하게 보내고 싶은 사람을 대상으로, 자투리 시간에 적합한 온라인/오프라인 활동을 `개인 맞춤형으로 추천`함으로써 자투리 시간을 `온전한 ‘내 시간’`으로 만들어갈 수 있도록 함 + +### 사용자에게 제공하는 혜택 + +- **`인식의 전환`** : 자투리 시간을 ‘무엇인가를 제대로 하기에 애매한 시간’이 아닌 ‘무엇이든 가볍게 시도해볼 수 있는 시간’으로 인식을 전환시킨다. +- **`‘나의’`** : **추천 온보딩**을 통해 오로지 ‘나의’ 자투리 시간을 만들어 갈 수 있도록 한다. +- **`부담없는`** : 부담 없이 일상 속에서 자투리 시간을 유의미하게 보낼 수 있는 **활동을 추천**해준다. +- **`쌓아가는`** : 활용한 자투리 시간을 **아카이빙**을 통해 쌓아가며 효능감/성취감을 느낄 수 있도록 한다. + +### 서비스 플로우 + +> **IA** + +![IA](https://github.com/user-attachments/assets/d804d0ea-1416-46a2-adc5-f7b2a3931d52) +> **전체 서비스 플로우** +> + +![전체 서비스 플로우](https://github.com/user-attachments/assets/36424b42-6082-4368-ba1a-6023a1bc51b4) + +> **기능별 세부 플로우** +> + +**`로그인 플로우`** + +![로그인 플로우](https://github.com/user-attachments/assets/f0b64c43-278e-4b3a-aef3-19e8fc91ea46) + +**`메인홈 - 추천 플로우`** + +![메인홈 플로우](https://github.com/user-attachments/assets/261afe83-0b6e-4840-9c99-b1948f3cf8f3) + +**`아카이빙 플로우`** + +![아카이빙 플로우](https://github.com/user-attachments/assets/c8ef4ad2-17d6-4b69-b3e8-37cc2a4917ee) + +**`마이페이지 플로우`** + +![마이페이지 플로우](https://github.com/user-attachments/assets/02684727-2900-4d1d-99d5-5c59f7f49a85) + +### **서비스 포인트 (참신성, 차별성 등)** + +> **유사 서비스 분석** + +| 서비스명 | 서비스 유형 | 메인 기능 및 특성 | 사용 목적 | +|-------------------|-------------------------|-------------------------------------------------------------------------------------------------------|---------------------------------------------------| +| **오늘 뭐할지 GetGPT** | AI 추천 | ChatGPT형 AI 추천 서비스
- 하루 할 일 추천 GPT
- 프롬프트 작성 필요
- 기본 질문 존재
- 아카이빙 불가능 | GPT와의 대화를 통해 하루 할 일 추천 가능 | +| **투두메이트** | 시간 활용 지원 (일정 관리) | 할 일 및 목표 리스트 생성
- 우선순위 설정
- 일정 알림 기능
- 완료 체크를 통한 성취감 제공 | 일정 관리 및 효율적 시간 분배를 통한 생산성 향상 | +| **마이루틴** | 시간 활용 지원 (루틴 관리) | 개인 맞춤형 루틴 설정 및 계획
- 알림을 통한 지속적 습관 형성
- 활동 기록 및 성과 시각화 | 습관 형성 및 시간 관리 개선을 통해 생산성 향상, 목표 달성 도움 | +| **조각조각** | 시간 활용 지원 및 AI 활용 추천 | 자투리 시간 활용 방법 추천
- 추천 활동 유형/카테고리의 다양성
- 아카이빙을 통한 시간 사용 분석 및 효용성 향상 | 버려지는 자투리 시간을 유의미한 활동으로 전환 | + + +> 💫 **참신성 & 차별성** +> +>- 자투리 시간에 대한 인식 변화를 통한 사회적 임팩트 부여 + > - 무엇인가를 제대로 하기에 애매한 시간 → 무엇이든 가볍게 시도해볼 수 있는 시간 +>- 온라인 활동 추천 + > - 사용자가 입력한 추천 온보딩 데이터를 기반으로 다양한 활동 추천 +> - 모든 사용자가 쉽게 접근할 수 있는 온라인 활동을 추천함으로써 시공간적 제약을 최소화하여 자투리 시간을 활용할 수 있도록 함 +>- 오프라인 추천 + > - 사용자 위치를 기반으로 방문 장소까지 구체적인 추천 +> - 사용자가 일상 속에서 쉽게 지나쳤던 장소 혹은 숨겨진 장소들을 재발견할 수 있도록 함으로써 지역 사회를 활성화 + +### 핵심 기능 + +| **기능** | **설명** | +|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **추천 온보딩** | 활동 추천을 위해 진행하는 온보딩으로, 사용자가 현재 상황에 따라 올바른 추천을 받을 수 있도록 합니다.
- **시간 선택**: 현재 사용할 수 있는 자투리 시간을 선택합니다. (10분 이상)
- **온라인/오프라인 선택**: 현재 사용자가 위치하고 있는 공간이 실내인지 실외인지 선택합니다.
- **활동 키워드 선택**: 사용자가 선호하는 활동의 형태를 선택합니다. | +| **활동 추천** | 앞선 온보딩을 기반으로 사용자에게 맞는 활동을 추천합니다.
- **온라인 활동**: 사용자의 휴대폰을 통해 할 수 있는 온라인 활동 추천
- **오프라인 활동**: 사용자의 현재 위치를 기반으로 할 수 있는 오프라인 활동 추천

사용자는 각 주제 별로 추천되는 5개의 활동 중 원하는 활동을 선택합니다.
만약 오프라인 활동에서 특정 장소 선택 시, 해당 장소의 위치 링크를 연결합니다. | +| **아카이빙** | 사용자가 해당 활동을 끝내고 나면, 해당 기록이 저장됩니다.
저장된 기록은 트리맵 형태의 ‘활동 키워드’와 캘린더 형태의 ‘활동 캘린더’로 확인할 수 있습니다. | + + + +### 서비스 비즈니스 모델 +오프라인 추천 시 매장과 제휴 + +제휴 매장을 오프라인 추천해줌으로써 비즈니스 모델 구현한다. + +1. 오프라인 매장과의 제휴 체결한다. +2. 해당 매장이 사용자의 온보딩 정보와 부합하는 경우, 오프라인 활동에서 추천한다. + +> 💵 **수익구조** +> 매장 → 조각조각 : 제휴 광고 수수료 제공 +> 조각조각 → 사용자 : 제휴 매장의 광고를 맞춤형으로 제공 + +> 📌 **기대효과** +> 매장 : 사용자 데이터를 바탕으로 한 개인화 된 추천을 통해 맞춤형 광고 가능 +> 사용자 : 현재 접근 가능한 맞춤형 매장을 추천 받을 수 있음 + +린캔버스 + +![도식화](https://github.com/user-attachments/assets/9c3337ba-d18a-4209-9194-6e0ce6e4eca9) + + +## 🎨 디자인 (Design) + +### 로고 및 디자인 시스템 + +> **디자인 컨셉** + +> ⏰ +> **조각조각의 디자인 컨셉** +> 조각조각은 우리가 일상 속에서 모을 수 있는 자투리 시간을 **‘시간 조각’** 이라고 정의합니다. +> 조각조각은 우리가 평소에 흘려보내던 일상 속의 자투리 시간을 색다르게 활용하게 함으로써 +> ‘시간 조각’을 모으는 경험을 제공합니다. 조각조각 내에서의 시간 조각은 엔터테인먼트, 소셜, 건강, +> 자기개발, 문화 / 예술, 휴식 크게 6개 종류로 나뉩니다. 각각의 조각들은 분야에 맞게 형상화 되어있습니다. (ex: 휴식은 머그컵이라는 오브제로 형상화) +> +> **형상화된 시간 조각을 찾고, 모으고, 쌓고, 보면서 조금 더 색다르고 의미있는 시간을 찾길 도와줍니다.** + +![design](https://github.com/user-attachments/assets/b3de7f66-42b8-48a3-b009-d9d31c077f32) + +![design 1](https://github.com/user-attachments/assets/b600155d-c451-4238-8955-e18971039d4c) + +![design 2](https://github.com/user-attachments/assets/3fb72ba5-4263-4b2f-9bf1-adb6dfb613ae) + +> ❔**왜 도트에서 clay 질감의 3d 그래픽으로 전환하게 되었나요?** +> +> 기존 도트 그래픽이 가진 문제점을 해결하고자 했습니다. +> 1. 사각형, 각진 그래픽으로 인해 동적인 느낌이 부족해 보인다는 점 +> 2. 단순히 도트 그래픽으로는 서비스 GUI를 완성도 있게 보여줄 수 없다는 한계점 +> 3. 도트 그래픽과 UI 컴포넌트의 조합 및 비중 설정을 잘못하게 될 시 서비스가 어수선하게 보일 수 있다는 리스크 +> 4. 초기 기획했던 가볍고 마치 간식을 꺼내 먹는 듯한 느낌을 추구하는 서비스 무드보다는 게임/게이미피케이션 서비스 무드에 가깝게 느껴지는 문제점 +> +> → **서비스 무드와 콘텐츠 성격, 높은 디자인적 완성도 측면을 고려하여** 그래픽 디자인을 변경하였습니다. + +> ❔**그렇다면 왜 clay 질감의 3d 그래픽인가요?** +> +> UI 요소나 컨텐츠가 많이 들어가지 않는 조각조각의 서비스 특성상 그래픽을 좀 더 다이나믹하게 보여줄 수 있는 그래픽이 필요하다고 판단했습니다. +> 단순 도트 그래픽으로는 화면 요소를 입체감 있게, 그리고 동적으로 보이게 구성하기가 어렵기 때문에 대안으로 다양한 3d 그래픽을 시도하게 되었습니다. +> 여러 대안 중 질감이 덜 들어가는 clay 질감의 그래픽이 가장 가벼운 느낌을 주었고, 서비스 무드와 매치하는 요소로 판단하여 clay 질감을 활용하게 되었습니다. + + +### 화면 디자인 + +![design 3](https://github.com/user-attachments/assets/cb701119-3e5d-4355-b36d-68126782ec9a) + +![design 4](https://github.com/user-attachments/assets/0ecc4b38-df03-43a7-9737-ea86a022b42b) + +![design 5](https://github.com/user-attachments/assets/f83bd29e-be7f-45f8-810c-764cf0982594) + +![design 6](https://github.com/user-attachments/assets/61e2e55b-6ca7-406a-92d7-f742d07661c3) + +![design 7](https://github.com/user-attachments/assets/7d4eb594-0c84-4953-a7c3-5186f0ba7f93) + +![design 8](https://github.com/user-attachments/assets/e25a099b-197a-4f7e-addd-4eb932609456) + +![design 9](https://github.com/user-attachments/assets/f85a7b0f-1bf0-4209-8861-a99d7fcae7b7) + +![design 10](https://github.com/user-attachments/assets/ec805e77-35c1-4f6a-80f9-0669c92e7faf) + +# 💻 개발(Development) + +> **Github URL:** [https://github.com/KUSITMS-30th-TEAM-C](https://github.com/KUSITMS-30th-TEAM-C) +> +> **배포 URL:** [https://cnergy.kro.kr/](https://cnergy.kro.kr/) + +## 개발 환경과 사용 기술 스택 + +### ❇️ 프론트엔드 기술 스택 및 선정 이유 + +- **Next.js 14**: + - **서버 사이드 렌더링(SSR)** 및 **정적 사이트 생성(SSG)** 지원으로 SEO 향상과 빠른 페이지 로딩 제공. + - **Full-stack 기능** 제공으로 API 라우팅과 프론트엔드를 통합할 수 있어 효율적인 개발 가능. + - **Edge Functions**와 같은 최신 성능 최적화 기능을 활용할 수 있음. +- **React 18**: + - **Concurrent Mode**를 통해 성능을 향상하고, 대규모 애플리케이션에서 더욱 부드러운 UI 렌더링. + - 최신 **Hook 기반 API**로 코드 간결성 및 상태 관리 향상. + - 커뮤니티와 생태계가 매우 크고, 다양한 서드파티 라이브러리와의 호환성이 좋음. +- **Tailwind CSS**: + - **유틸리티 우선 CSS 프레임워크**로, 빠르게 일관된 스타일링 가능. + - 클래스 단위로 CSS를 관리하기 때문에 유지보수성이 높고, 불필요한 CSS 코드 생성을 최소화. + - 커스텀 디자인을 쉽게 적용하면서도 **반응형 디자인**을 유연하게 구현할 수 있음. +- **eslint - airbnb**: + - **코드 스타일 일관성**을 유지하고, **버그를 예방**하기 위해 AirBnB 스타일 가이드를 활용한 Linting 도구. + - 코드 품질 향상과 팀 간 협업 시 일관된 코딩 스타일을 유지. +- **Prettier**: + - **자동 코드 포매팅**을 통해 코드 스타일을 일관되게 유지. + - 개발자의 생산성 향상과 코드 리뷰 시 시각적 노이즈를 줄일 수 있음. +- **Storybook**: + - **UI 컴포넌트 개발 및 테스트 도구**로, 컴포넌트 단위 개발을 효율적으로 진행. + - **디자인 시스템**을 관리하고, 컴포넌트 간의 재사용성을 높임. +- **TanStack Query & ContextAPI**: + - 효율적인 비동기 처리를 위해 사용 +- **CI/CD (GitHub Actions, NCP, Docker)**: + - **GitHub Actions**: 코드 변경 시 자동화된 빌드, 테스트, 배포 파이프라인을 설정하여 개발 프로세스의 효율성 증대. + - **NCP (Naver Cloud Platform)**: 안정적인 인프라 제공 및 한국 지역 기반 서비스 운영 시 유리. + - **Docker**: 개발 환경을 컨테이너화하여 일관성 있는 환경에서 애플리케이션 배포 가능. +- **Zustand**: + - **가벼운 상태 관리 라이브러리**로, Redux보다 간단하고 코드가 간결해지며, 리액티브한 글로벌 상태 관리에 적합. + +### ❇️ 백엔드 기술 스택 및 선정 이유 + +- **Java 17** + - LTS(Long-Term Support) 버전으로 2029년 9월까지 지원합니다. + - 최신 LTS 버전인 JDK21을 바로 사용하는 것보다 그 전 LTS 버전인 JDK 17을 사용하여 추후 JDK 21로 마이그레이션시 영향을 줄일 수 있기 때문입니다. + - Spring Boot 3.0부터는 JDK 17 이상부터 지원하므로 JDK17을 사용하였습니다. +- **Spring Boot 3.3.4** + - 이번 프로젝트에서는 WebSocket을 이용한 채팅 기능을 서비스에 도입하지는 않았지만, Spring Boot 3.3 버전에서 제공하는 WebSocket의 가상 스레드 지원을 활용하여 동시성을 효율적으로 처리할 수 있는 점을 고려해 사용했습니다. +- **Spring Data JPA** + - SQL을 직접 작성하지 않고 객체지향적인 코드로 데이터베이스를 다루기 위해서 JPA를 사용하고, Spring 프레임워크에서 JPA를 쉽게 사용할 수 있는 모듈인 Spring Data JPA를 지원하기 때문에 사용하였습니다. +- **MySQL 8.x** + - 5 버전에 비해 향상된 성능, 강화된 보안 기능, 공간 데이터 처리 등을 사용하기 위해 사용했습니다. +- **Docker** + - 개발 및 배포 환경을 쉽게 컨테이너화하기 위해 사용했습니다. +- **Github Actions** + - Github와 하나로 통일된 환경에서 CI를 수행하기 위해 사용했습니다. +- **Naver Cloud Platform** + - 한국에서 만든 클라우드 서비스로 서버, AI 등 프로젝트에 필요한 서비스를 사용했습니다. +- **Sentry** + - 에러 로그 모니터링과 중앙집중적 에러 로그 관리를 위해 사용했습니다. +- **Swagger** + - 클라이언트, 서버 간 API 명세서 용도로 사용했습니다. +- **OpenAI** + - OpenAI는 다양한 온라인 활동 추천을 제공하는 데 강점이 있습니다. 이를 통해 사용자의 관심사에 맞는 폭넓고 창의적인 추천을 만들어 더 다양한 활동 선택지를 제공할 수 있어 사용하게 되었습니다. +- **RabbitMQ** + - RabbitMQ의 “Delayed Message Exchange Plugin”을 활용하여, 종료 로직을 특정 시간만큼 지연된 메시지로 전달하는 방식으로 구현했습니다. + - 비동기적으로 종료 작업을 처리하여 주요 트랜잭션과 분리할 수 있는 장점이 있어 사용했습니다. + +## 🟩 NCP 사용 스택 + +- **Server** + - 백엔드 배포 서버로 사용했습니다. + - 프론트엔드 배포 서버로 사용했습니다. +- **VPC** + - 클라우드 내 전용 네트워크를 확보하기 위해 VPC를 사용했습니다. + - 현재 프론트엔드와 백엔드가 하나의 VPC내 다른 Subnet으로 구성되어있습니다. + - 추후 백엔드 Subnet을 Private으로 설정해 외부접근을 제한하고, 프론트엔드 서버를 통해서만 접근할 수 있도록해 보안을 강화할 예정입니다. +- **Container Registry** + - 백엔드 및 프론트엔드 모두 Docker를 사용해 배포를 진행합니다. Public Registry에 이미지를 저장할 경우, 민감한 정보들을 관리하기 어렵기에 사설 Registry인 NCP Container Registry를 사용했습니다. + - Container Registry 의 경우 레지스트리에 등록된 이미지의 취약점을 분석해 정보를 제공해줍니다. 이를 통해 컨테이너의 취약점을 제거해 컨테이너를 더욱 안전하게 사용할 수 있습니다. +- **Naver Clova Studio** + - 한국 사람들이 가장 많이 사용하는 검색엔진의 데이터를 활용하여 개발되었기 때문에, 한국 사용자에게 활동을 추천하는 프로젝트에 적합하다 판단해 사용합니다. + - 사용자에게 현재 상황과 취향을 입력받아 활동 추천 시 Clova AI를 사용합니다. + - 모델을 튜닝하여 사용자에게 다양한 활동을 추천합니다. +- **NAVER Object Storage** + - 오브젝트 스토리지 버킷을 NCP CDN과 연결하여 CDN 서버에서 빠르고 효율적으로 이미지 제공 + + +## 🏛️ 시스템 아키텍처 + +![시스템 아키텍처](https://github.com/user-attachments/assets/566c2abc-8974-4135-a78b-ccf60c8ed6d8) + +## 🧱 ERD + +![ERD](https://github.com/user-attachments/assets/487b3527-c172-4e6c-84b9-a84dd45e8c48) + +## 🌊 개발부터 배포까지의 워크플로우 + +> **1. 개발 단계** +> +- Git을 사용한 버전 관리 +- 브랜치 전략 적용 (Git-flow) +- Pull Request를 통한 코드 리뷰 + - reviewer의 approve 없이 dev 브랜치에 push 불가 + +> **2. CI (지속적 통합) 구축** +> +- GitHub Actions 사용 +- 자동 빌드 및 테스트 실행 +- Docker 이미지 빌드 및 Registry 푸시 + +> **3. 배포 준비** +> +- Docker Compose 를 통해 배포(SpringBoot, Redis, Rabbit MQ) + +> **4. 배포 프로세스** +> +1. CI/CD 파이프라인에서 새 Docker 이미지 빌드 및 푸시 +2. 운영 서버에서 최신 이미지 pull +3. Docker Compose로 서비스 업데이트 + +> **5. 모니터링 및 로깅 (Sentry 중심)** +> +- Sentry 대시보드 모니터링 +- 실시간 에러 추적 +- Grafana와 Prometheus를 활용한 서버 모니터링 + +## 💻 프론트 - 비동기 데이터 페칭 최적화 및 이미지 로딩 성능 향상 + +### **AsyncBoundary+tanstack-query를 이용한 데이터 페칭** + +- tanstack-query를 이용한 데이터 캐싱 +- SSR환경에서 사용 가능한 Suspense 구현 +- AsyncBoundary를 통한 비동기 데이터페칭 에러/로딩 상태 선언적 관리 +- AsyncBoundaryWithQuery 구현 : tanstack-query의 useQueryErrorResetBoundary를 활용하여 데이터 페칭 중 오류 발생 시 데이터 리페치 +- querykey가 바뀔때마다 컴포넌트 깜빡임(fallback 노출) 이슈 + - useTransition 훅을 활용하여 상태 업데이트를 UI 렌더링보다 지연시킴으로써 데이터 페칭 중 발생하는 깜빡임을 방지 + +### **NCP CDN과 Next.js Image태그를 이용한 빠른 이미지 로딩** + +- NCP 오브젝트 스토리지와 연동하여 정적 리소스의 캐싱 및 전달을 효율적으로 수행 +- **자동 최적화**: 브라우저와 디바이스에 맞는 해상도와 크기로 이미지를 자동 변환 +- **레이아웃 시프트 방지**: 이미지의 고정된 크기를 제공하여 CLS(Cumulative Layout Shift) 이슈 방지 + +## 💻 백엔드 - 아키텍처 설계: 도메인 주도 설계(DDD) 적용 + +- 도메인을 중심으로 아키텍처를 설계하여 비즈니스 로직을 명확하게 분리하였습니다. +- 표현, 응용, 도메인, 인프라스트럭처 계층으로 분리하여 각 계층의 책임을 명확히 정의하고, 역할에 맞는 책임 분담을 실현하였습니다. +- 의존 역전 원칙(DIP)을 적용하여 상위 계층이 하위 계층에 의존하지 않도록 하여, 계층 간의 결합도를 최소화하고 유연하고 확장 가능한 구조를 구현하였습니다. +- 외부 모듈의 영향을 최소화하기 위해 도메인 모델에 JPA를 직접 결합하지 않고, POJO 객체를 사용하여 도메인의 순수성을 유지하였습니다. JPA 엔티티는 인프라 계층에서만 관리하여 도메인과 인프라를 명확하게 분리하였습니다. +- 조회는 DAO를 활용하여 CQRS 패턴을 적용, 데이터 조회와 변경을 명확히 분리하여 각 책임을 독립적으로 처리하고 성능과 유지보수성을 향상시켰습니다. From 181852c0b1879978ffe881592b8515d518ab956c Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 29 Nov 2024 17:10:19 +0900 Subject: [PATCH 418/478] =?UTF-8?q?fix:=20(#174)=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=EC=97=90=EC=84=9C=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EC=97=90=20=EC=B4=88=EC=84=B1=20=EC=9E=85=EB=A0=A5=EC=9D=B4=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/OnboardingSignUpRequest.java | 2 +- .../service/ValidateNicknameService.java | 2 +- .../service/ValidateNicknameServiceTest.java | 21 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java b/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java index 03648b55c..d37e3936d 100644 --- a/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java +++ b/src/main/java/spring/backend/auth/presentation/dto/request/OnboardingSignUpRequest.java @@ -6,7 +6,7 @@ public record OnboardingSignUpRequest( - @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,6}$", message = "닉네임은 한글, 영문, 숫자 조합 6자 이내로 입력해주세요.") + @Pattern(regexp = "^[a-zA-Z0-9가-힣ㄱ-ㅎ]{1,6}$", message = "닉네임은 한글, 영문, 숫자 조합 6자 이내로 입력해주세요.") @Schema(description = "닉네임", example = "조각조각") String nickname, diff --git a/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java b/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java index 5d3405712..9e94646ea 100644 --- a/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java +++ b/src/main/java/spring/backend/member/domain/service/ValidateNicknameService.java @@ -22,7 +22,7 @@ public boolean validateNickname(String nickname) { log.error("[ValidateNicknameService] Nickname is smaller than 6 characters"); return false; } - if (!nickname.matches("^[a-zA-Z0-9가-힣]+$")) { + if (!nickname.matches("^[a-zA-Z0-9가-힣ㄱ-ㅎ]{1,6}$")) { log.error("[ValidateNicknameService] Nickname is invalid"); return false; } diff --git a/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java b/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java index 6e78e0976..a6113d7be 100644 --- a/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java +++ b/src/test/java/spring/backend/member/domain/service/ValidateNicknameServiceTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -10,6 +12,7 @@ import spring.backend.member.domain.value.Role; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,16 +49,6 @@ void throwExceptionWhenNicknameLengthIsInvalid() { assertFalse(validateNicknameService.validateNickname(nickname)); } - @Test - @DisplayName("닉네임 형식이 유효하지 않을 때 예외가 발생한다.") - void throwExceptionWhenNicknameFormatIsInvalid() { - // Given - String nickname = "조각ㅈㄱ"; - - // When & Then - assertFalse(validateNicknameService.validateNickname(nickname)); - } - @Test @DisplayName("이미 등록된 닉네임일 경우 예외가 발생한다.") void throwExceptionWhenNicknameIsAlreadyRegistered() { @@ -69,4 +62,12 @@ void throwExceptionWhenNicknameIsAlreadyRegistered() { // Mock 객체 정상 동작 확인 verify(memberRepository).existsByNicknameAndRole(nickname, Role.MEMBER); } + + @ParameterizedTest + @DisplayName("올바른 형식의 이름일 경우 성공한다.") + @ValueSource(strings = {"ㅍ카칩", "ㄱ", "포ㅋ칩", "포카ㅊ", "조각조각ㅈㄱ", "q", "qwerty"}) + void validateNicknameWithInitialConsonants(String nickname) { + // When & Then + assertTrue(validateNicknameService.validateNickname(nickname)); + } } From 9b805500a6903464ff2cb6e2ea70a893e5670f4b Mon Sep 17 00:00:00 2001 From: anxi01 Date: Fri, 29 Nov 2024 17:11:22 +0900 Subject: [PATCH 419/478] =?UTF-8?q?fix:=20(#174)=20=EB=B9=A0=EB=A5=B8?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=83=9D=EC=84=B1=EC=8B=9C=20=EC=B4=88?= =?UTF-8?q?=EC=84=B1=20=EC=9E=85=EB=A0=A5=EC=9D=B4=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dto/request/QuickStartRequest.java | 2 +- .../backend/quickstart/dto/request/QuickStartRequestTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java b/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java index 53f839a4a..048c60fc1 100644 --- a/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java +++ b/src/main/java/spring/backend/quickstart/presentation/dto/request/QuickStartRequest.java @@ -7,7 +7,7 @@ public record QuickStartRequest( @NotNull(message = "이름은 필수 입력 항목입니다.") - @Pattern(regexp = "^(?!\\s)([a-zA-Z0-9가-힣]+(\\s[a-zA-Z0-9가-힣]+)*)?$", message = "이름은 한글, 영문, 숫자 및 공백만 입력 가능하며, 공백으로 시작하거나 끝날 수 없고, 연속된 공백이 없어야 합니다.") + @Pattern(regexp = "^(?!\\s)([a-zA-Z0-9가-힣ㄱ-ㅎ]+(\\s[a-zA-Z0-9가-힣ㄱ-ㅎ]+)*)?$", message = "이름은 한글(초성 포함), 영문, 숫자 및 공백만 입력 가능하며, 공백으로 시작하거나 끝날 수 없고, 연속된 공백이 없어야 합니다.") @Size(max = 10, message = "최대 10자까지 입력 가능합니다.") @Schema(description = "빠른 시작 이름", example = "등교") String name, diff --git a/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java index b767a8b13..f96718531 100644 --- a/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java +++ b/src/test/java/spring/backend/quickstart/dto/request/QuickStartRequestTest.java @@ -42,7 +42,7 @@ void whenNameIsNull_thenValidationFails() { @ParameterizedTest @DisplayName("올바른 형식의 이름일 경우 성공한다.") - @ValueSource(strings = {"등교", "이름테스트", "John Doe", "사용자1"}) + @ValueSource(strings = {"등교", "이름테스트", "띄어쓰기 포함 10", "사용자1", "ㄱ", "ㄱ나다라ㅁ바사ㅇㅈㅋ"}) void whenNameIsValid_thenValidationSucceeds(String name) { QuickStartRequest request = new QuickStartRequest(name, 12, 30, "오전", 300, Type.OFFLINE); Set> violations = validator.validate(request); @@ -58,7 +58,7 @@ void whenNameIsInvalid_thenValidationFails(String name) { Set> violations = validator.validate(request); assertThat(violations).isNotEmpty(); - assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 한글, 영문, 숫자 및 공백만 입력 가능하며")); + assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 한글(초성 포함), 영문, 숫자 및 공백만 입력 가능하며,")); } @Test From e2405040b6f8e74069b0d0e6ca7b114aecefa9e7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 30 Nov 2024 01:12:49 +0900 Subject: [PATCH 420/478] =?UTF-8?q?fix:=20(#176)=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=9E=A5=EC=86=8C=EC=9D=98=20url=EC=9D=B4?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0,=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=EB=A7=B5=20=EA=B2=80=EC=83=89=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=84=98=EA=B8=B4=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index d3bb837ce..0a9576b9e 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import spring.backend.activity.domain.value.Keyword; import spring.backend.activity.domain.value.Keyword.Category; @@ -13,10 +14,7 @@ import spring.backend.recommendation.infrastructure.clova.exception.ClovaErrorCode; import spring.backend.recommendation.infrastructure.map.kakao.dto.response.KakaoMapResponse; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Random; +import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -36,6 +34,9 @@ public class GetRecommendationsFromClovaService { private static final String LINE_SEPARATOR = "\n"; private static final int ONLINE_AND_OFFLINE_RECOMMENDATION_COUNT = 3; + @Value("${kakao.map-uri}") + private String kakaoMapUri; + private final RecommendationProvider recommendationProvider; private final PlaceInfoProvider kakaomapPlaceInfoProvider; private final ImageConverter imageConverter; @@ -103,7 +104,11 @@ private List fetchRecommendations(AIRecommendationR if (placeInfo.documents() != null && !placeInfo.documents().isEmpty()) { mapx = placeInfo.documents().get(0).x(); mapy = placeInfo.documents().get(0).y(); - placeUrl = placeInfo.documents().get(0).placeUrl(); + if (placeInfo.documents().get(0).placeUrl().isEmpty()) { + placeUrl = kakaoMapUri; + } else { + placeUrl = placeInfo.documents().get(0).placeUrl(); + } } i++; @@ -138,7 +143,7 @@ private String parsedKeywordText(String keywordText) { List validKeywords = Arrays.stream(keywordText.split(",")) .map(String::trim) - .filter(this::isValidKeyword) + .map(this::getValidKeywordDescription) .toList(); if (validKeywords.isEmpty()) { @@ -150,11 +155,23 @@ private String parsedKeywordText(String keywordText) { return validKeywords.get(randomIdx); } - private boolean isValidKeyword(String keyword) { - return Arrays.stream(Keyword.Category.values()) - .map(Keyword.Category::getDescription) - .collect(Collectors.toSet()) - .contains(keyword.trim()); + private String getValidKeywordDescription(String keyword) { + try { + Keyword.Category category = null; + + if (Arrays.stream(Keyword.Category.values()) + .map(Enum::name) + .anyMatch(name -> name.equalsIgnoreCase(keyword.trim()))) { + category = Keyword.Category.valueOf(keyword.trim().toUpperCase()); + } else { + category = Keyword.Category.from(keyword.trim()); + } + + return category != null ? category.getDescription() : null; + } catch (IllegalArgumentException e) { + log.warn("Invalid keyword: {}", keyword, e); + return null; + } } private AIRecommendationRequest filteredValidRecommendations(AIRecommendationRequest clovaRecommendationRequest) { From 376572718439e0151dc269222283882ce86df97b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 30 Nov 2024 13:01:16 +0900 Subject: [PATCH 421/478] =?UTF-8?q?fix:=20(#178)=20Prompt=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GetRecommendationsFromClovaService.java | 2 +- .../infrastructure/clova/dto/request/ClovaStudioPrompt.java | 1 + .../presentation/dto/request/AIRecommendationRequest.java | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index 0a9576b9e..d2a65dfc2 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -104,7 +104,7 @@ private List fetchRecommendations(AIRecommendationR if (placeInfo.documents() != null && !placeInfo.documents().isEmpty()) { mapx = placeInfo.documents().get(0).x(); mapy = placeInfo.documents().get(0).y(); - if (placeInfo.documents().get(0).placeUrl().isEmpty()) { + if (Objects.equals(placeInfo.documents().get(0).placeUrl(), "")) { placeUrl = kakaoMapUri; } else { placeUrl = placeInfo.documents().get(0).placeUrl(); diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index bcc55b4a0..ee4804ba0 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -16,6 +16,7 @@ public class ClovaStudioPrompt { 1. 활동 타입이 OFFLINE, ONLINE_AND_OFFLINE일 경우: - 입력된 활동 키워드, 시간 그리고 장소를 고려하여 다양한 오프라인 활동을 추천. - 추천되는 활동의 플랫폼은 한국 지역을 추천. + 2. 입력받은 장소에서 5km 이내에 있는 활동 또는 장소를 추천. --- 활동 키워드별 정의와 예시: 1. SELF_DEVELOPMENT diff --git a/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java b/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java index 68fbda8dc..8891a12be 100644 --- a/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java +++ b/src/main/java/spring/backend/recommendation/presentation/dto/request/AIRecommendationRequest.java @@ -19,10 +19,10 @@ public record AIRecommendationRequest( Type activityType, @NotNull(message = "키워드는 필수 입력 항목입니다.") - @Schema(description = "활동 키워드", example = "[\"RELAXATION\",\"CULTURE_ART\"]") + @Schema(description = "활동 키워드", example = "[\"RELAXATION\",\"ENTERTAINMENT\"]") Keyword.Category[] keywords, - @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 강남구") + @Schema(description = "위치(activityType이 OFFLINE, ONLINE_AND_OFFLINE인 경우에만 필요합니다.)", example = "서울시 마포구 공덕동") String location ) { } From 8b9f1712f9a0ce5a7b08302293210c3ed22671ad Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 2 Dec 2024 13:45:30 +0900 Subject: [PATCH 422/478] =?UTF-8?q?refactor:=20(#180)=20Prompt=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/clova/dto/request/ClovaStudioPrompt.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java index ee4804ba0..6648e2b37 100644 --- a/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java +++ b/src/main/java/spring/backend/recommendation/infrastructure/clova/dto/request/ClovaStudioPrompt.java @@ -9,7 +9,7 @@ public class ClovaStudioPrompt { 1. 자투리 시간: 사용자가 활용할 수 있는 시간 (예: 10분, 60분 등). 2. 활동 타입: - OFFLINE, ONLINE_AND_OFFLINE: 오프라인 활동 추천 - 3. 활동 키워드: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술 등). + 3. 활동 키워드: 사용자가 관심 있는 주제 (예: 휴식, 자기개발, 문화/예술, 엔터테인먼트 등). 4. 장소 : 사용자 위치 --- 추천 기준: From 4992ee3ddd125f1f0174d0c7db2d78e7d1cc33f7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 2 Dec 2024 13:46:01 +0900 Subject: [PATCH 423/478] =?UTF-8?q?refactor:=20(#180)=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B0=92=EC=97=90=20=EC=9D=BC=EC=B9=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetRecommendationsFromClovaService.java | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java index d2a65dfc2..201598613 100644 --- a/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java +++ b/src/main/java/spring/backend/recommendation/application/GetRecommendationsFromClovaService.java @@ -123,8 +123,7 @@ private List fetchRecommendations(AIRecommendationR Keyword keyword = null; if (i + 1 < recommendations.length && KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).find()) { String keywordText = KEYWORD_PREFIX_PATTERN.matcher(recommendations[i + 1].trim()).replaceFirst("").trim(); - String parsedKeywordText = parsedKeywordText(keywordText); - Category category = convertClovaResponseKeywordToKeywordCategory(parsedKeywordText); + Category category = parsedKeywordTextToCategory(keywordText); keyword = Keyword.create(category, imageConverter.convertToImageUrl(category)); i++; } @@ -136,41 +135,36 @@ private List fetchRecommendations(AIRecommendationR return clovaResponses; } - private String parsedKeywordText(String keywordText) { + private Category parsedKeywordTextToCategory(String keywordText) { if (keywordText == null || keywordText.isEmpty()) { return null; } - List validKeywords = Arrays.stream(keywordText.split(",")) + List validKeywordCategories = Arrays.stream(keywordText.split(",")) .map(String::trim) - .map(this::getValidKeywordDescription) + .map(this::convertClovaResponseKeywordToKeywordCategory) .toList(); - if (validKeywords.isEmpty()) { + if (validKeywordCategories.isEmpty()) { return null; } RANDOM.setSeed(System.nanoTime()); - int randomIdx = RANDOM.nextInt(validKeywords.size()); - return validKeywords.get(randomIdx); + int randomIdx = RANDOM.nextInt(validKeywordCategories.size()); + return validKeywordCategories.get(randomIdx); } - private String getValidKeywordDescription(String keyword) { + private Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { + if (keywordText == null || keywordText.isEmpty()) { + return null; + } try { - Keyword.Category category = null; - - if (Arrays.stream(Keyword.Category.values()) - .map(Enum::name) - .anyMatch(name -> name.equalsIgnoreCase(keyword.trim()))) { - category = Keyword.Category.valueOf(keyword.trim().toUpperCase()); - } else { - category = Keyword.Category.from(keyword.trim()); - } - - return category != null ? category.getDescription() : null; + return Category.valueOf(keywordText.trim()); } catch (IllegalArgumentException e) { - log.warn("Invalid keyword: {}", keyword, e); - return null; + return Arrays.stream(Category.values()) + .filter(category -> category.getDescription().equals(keywordText)) + .findFirst() + .orElse(null); } } @@ -228,17 +222,4 @@ private void validateClovaRecommendationRequestKeyword(AIRecommendationRequest c } } - private Category convertClovaResponseKeywordToKeywordCategory(String keywordText) { - if (keywordText == null || keywordText.isEmpty()) { - return null; - } - try { - return Category.valueOf(keywordText); - } catch (IllegalArgumentException e) { - return Arrays.stream(Category.values()) - .filter(category -> category.getDescription().equals(keywordText)) - .findFirst() - .orElse(null); - } - } } From adbdcb13fbadb1e60db4fbf6cf287b1772212327 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:06:06 +0900 Subject: [PATCH 424/478] =?UTF-8?q?refactor:=20(#110)=20AuthorizationInter?= =?UTF-8?q?ceptor=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=EC=9D=84=20=EC=BF=A0=ED=82=A4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interceptor/AuthorizationInterceptor.java | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java index 4a6a174bf..68e8d635f 100644 --- a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java @@ -1,52 +1,50 @@ package spring.backend.core.configuration.interceptor; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpMethod; +import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; -import java.lang.annotation.Annotation; - @Component @RequiredArgsConstructor +@Log4j2 public class AuthorizationInterceptor implements HandlerInterceptor { - public static final String AUTHORIZATION_HEADER = "Authorization"; - - public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer"; - private final JwtService jwtService; @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - if (HttpMethod.OPTIONS.name().equals(request.getMethod())) { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (isOAuthLoginRequest(request)) { return true; } - String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); - if (handler instanceof HandlerMethod) { - HandlerMethod handlerMethod = (HandlerMethod) handler; - Annotation authorizationAnnotation = handlerMethod.getMethodAnnotation(Authorization.class); - if (authorizationAnnotation != null) { - String token = extractToken(authorizationHeader); - jwtService.validateTokenExpiration(token); - } + + String accessToken = extractToken(request); + if (accessToken == null) { + log.error("쿠키에 토큰이 존재하지 않습니다."); + throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); } + jwtService.validateTokenExpiration(accessToken); return true; } - private String extractToken(String authorizationHeader) { - if (authorizationHeader == null) { - throw AuthenticationErrorCode.NOT_EXIST_HEADER.toException(); - } - try { - return authorizationHeader.split(AUTHORIZATION_BEARER_PREFIX)[1].replace(" ", ""); - } catch (Exception e) { - throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); + private boolean isOAuthLoginRequest(HttpServletRequest request) { + return request.getRequestURI().startsWith("/v1/oauth"); + } + + private String extractToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("access_token".equals(cookie.getName())) { + return cookie.getValue(); + } + } } + return null; } } From 6dc7a05fe3d25551d87d861981f83f847c177b56 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:06:52 +0900 Subject: [PATCH 425/478] =?UTF-8?q?refactor:=20(#110)=20LoginMemberArgumen?= =?UTF-8?q?tResolver=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20=EB=BD=91=EC=95=84=EB=82=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoginMemberArgumentResolver.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java index d91640cd8..d00ba90d4 100644 --- a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java @@ -1,5 +1,7 @@ package spring.backend.core.configuration.argumentresolver; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.core.MethodParameter; @@ -8,12 +10,12 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; import spring.backend.member.exception.MemberErrorCode; +import java.util.Arrays; import java.util.Optional; import java.util.UUID; @@ -22,10 +24,6 @@ @Log4j2 public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { - public static final String AUTHORIZATION_HEADER = "Authorization"; - - public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer"; - private final JwtService jwtService; private final MemberRepository memberRepository; @@ -37,21 +35,29 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - String authorizationHeader = webRequest.getHeader(AUTHORIZATION_HEADER); - String token = extractToken(authorizationHeader); + String token = extractToken(webRequest); + if (token == null) { + log.error("토큰이 존재하지 않습니다."); + throw new IllegalArgumentException("토큰이 존재하지 않습니다."); + } + UUID memberId = jwtService.extractMemberId(token); Member member = memberRepository.findById(memberId); return Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException); } - private String extractToken(String authorizationHeader) { - if (authorizationHeader == null) { - throw AuthenticationErrorCode.NOT_EXIST_HEADER.toException(); - } - try { - return authorizationHeader.split(AUTHORIZATION_BEARER_PREFIX)[1].replace(" ", ""); - } catch (Exception e) { - throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); + private String extractToken(NativeWebRequest request) { + HttpServletRequest httpRequest = request.getNativeRequest(HttpServletRequest.class); + if (httpRequest != null) { + Cookie[] cookies = httpRequest.getCookies(); + if (cookies != null) { + return Arrays.stream(cookies) + .filter(cookie -> "access_token".equals(cookie.getName())) + .findFirst() + .map(Cookie::getValue) + .orElse(null); + } } + return null; } } From c8a8bed434a7df947da690ad3e3eb260c53229b7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:08:12 +0900 Subject: [PATCH 426/478] =?UTF-8?q?refactor:=20(#110)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=97=90=20=ED=86=A0=ED=81=B0=EA=B3=BC=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=84=A3=EC=96=B4=EC=A4=80?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HandleOAuthLoginController.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index 54544d16c..066137968 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -1,11 +1,14 @@ package spring.backend.auth.presentation; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import spring.backend.auth.application.HandleOAuthLoginService; import spring.backend.auth.presentation.dto.response.LoginResponse; import spring.backend.core.presentation.RestResponse; +import spring.backend.auth.dto.response.LoginResponse; @RestController @RequestMapping("/v1/oauth/login") @@ -15,10 +18,20 @@ public class HandleOAuthLoginController { private final HandleOAuthLoginService handleOAuthLoginService; @GetMapping("/{providerName}") - public ResponseEntity> handleOAuthLogin(@RequestParam(value = "code", required = false) String code, - @RequestParam(value = "state", required = false) String state, - @PathVariable String providerName) { + public ResponseEntity handleOAuthLogin(@RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state", required = false) String state, @PathVariable String providerName) { LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); - return ResponseEntity.ok(new RestResponse<>(loginResponse)); + ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) + .httpOnly(true) + .build(); + + ResponseCookie roleCookie = ResponseCookie.from("user_role", loginResponse.role().toString()) + .httpOnly(true) + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, roleCookie.toString()) + .build(); } } From 8561da96d3b291b1e5b936b55b4413eae9bca296 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:23:41 +0900 Subject: [PATCH 427/478] =?UTF-8?q?refactor:=20(#110)=20AuthenticationErro?= =?UTF-8?q?rCode=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/exception/AuthenticationErrorCode.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 19fc8aa6d..578acc693 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -11,7 +11,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { NOT_EXIST_HEADER(HttpStatus.UNAUTHORIZED, "Authorization Header가 존재하지 않습니다."), - NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "Authorization Header에 Token이 존재하지 않습니다."), + NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "쿠키에 Token이 존재하지 않습니다."), NOT_MATCH_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "토큰의 형식이 맞지 않습니다."), INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED, "토큰의 서명이 올바르지 않습니다."), NOT_DEFINE_TOKEN(HttpStatus.UNAUTHORIZED, "정의되지 않은 토큰입니다."), @@ -28,7 +28,8 @@ public enum AuthenticationErrorCode implements BaseErrorCode { MISSING_COOKIE_VALUE(HttpStatus.BAD_REQUEST, "쿠키값이 존재하지 않습니다."), INVALID_MEMBER_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입을 위한 사용자 조건이 유효하지 않습니다."), NOT_EXIST_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입 요청이 유효하지 않습니다."), - INVALID_BIRTH_YEAR(HttpStatus.BAD_REQUEST, "출생년도는 현재 연도와 100년 전 사이여야 합니다."); + INVALID_BIRTH_YEAR(HttpStatus.BAD_REQUEST, "출생년도는 현재 연도와 100년 전 사이여야 합니다."), + FAILED_TO_EXTRACT_MEMBER_ID_FROM_EXPIRED_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "만료된 액세스 토큰에서 회원 ID를 추출하는데 실패했습니다."); private final HttpStatus httpStatus; @@ -38,4 +39,4 @@ public enum AuthenticationErrorCode implements BaseErrorCode { public DomainException toException() { return new DomainException(httpStatus, this); } -} \ No newline at end of file +} From eab029aee69213ca418befc56021cede1c055ef3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:24:06 +0900 Subject: [PATCH 428/478] =?UTF-8?q?refactor:=20(#110)=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89=EC=8B=9C=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20accessToken=EC=9D=84=20=EC=BF=A0=ED=82=A4=EC=97=90?= =?UTF-8?q?=20=EB=84=A3=EC=96=B4=EC=A4=80=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RotateAccessTokenController.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 86b2b47df..dc413d883 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -1,9 +1,12 @@ package spring.backend.auth.presentation; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; @@ -13,13 +16,22 @@ @RestController @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor +@Log4j2 public class RotateAccessTokenController { private final RotateAccessTokenService rotateTokenService; - @GetMapping + @PostMapping public ResponseEntity> rotateAccessToken( - @CookieValue(name = "refreshToken", required = false) String refreshToken + @CookieValue(name = "access_token", required = false) String accessToken ) { - return ResponseEntity.ok(new RestResponse<>(rotateTokenService.rotateAccessToken(refreshToken))); + RotateAccessTokenResponse rotateAccessTokenResponse = rotateTokenService.rotateAccessToken(accessToken); + ResponseCookie cookie = ResponseCookie.from("access_token", rotateAccessTokenResponse.accessToken()) + .httpOnly(true) + .path("/") + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .build(); } } From 72b69d596ef98909e2566cfbf461c873e8b7fbf1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:24:35 +0900 Subject: [PATCH 429/478] =?UTF-8?q?refactor:=20(#110)=20LoginResponse?= =?UTF-8?q?=EC=97=90=EC=84=9C=20refreshToken=EC=9D=84=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/dto/response/LoginResponse.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/spring/backend/auth/dto/response/LoginResponse.java diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java new file mode 100644 index 000000000..80e22f1fb --- /dev/null +++ b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java @@ -0,0 +1,6 @@ +package spring.backend.auth.dto.response; + +import spring.backend.member.domain.value.Role; + +public record LoginResponse(String accessToken, Role role) { +} From e0cc85db943cc65d2dd275f16a28b4a0e3cc3091 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:26:01 +0900 Subject: [PATCH 430/478] =?UTF-8?q?refactor:=20(#110)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20refreshToken=EC=9D=84=20redis=EC=97=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/HandleOAuthLoginService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 0536a09df..ce95f48a3 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -49,9 +49,11 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() .provider(provider) .email(oAuthResourceResponse.getEmail()) + .nickname(oAuthResourceResponse.getName()) .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - return LoginResponse.of(jwtService.provideAccessToken(member), refreshTokenService.saveRefreshToken(member), member); + refreshTokenService.saveRefreshToken(member); + return new LoginResponse(jwtService.provideAccessToken(member), member.getRole()); } } From e8c0e52dcfc8da13667a6b35d7a7474019ac0fcb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:26:48 +0900 Subject: [PATCH 431/478] =?UTF-8?q?refactor:=20(#110)=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=EB=90=9C=20accessToken=EC=97=90=EC=84=9C=20memberId=EB=A5=BC?= =?UTF-8?q?=20=EC=B6=94=EC=B6=9C=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/application/JwtService.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index c4ff710e0..b12ad3daa 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -8,6 +8,7 @@ import io.jsonwebtoken.security.SignatureException; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import spring.backend.auth.exception.AuthenticationErrorCode; @@ -23,6 +24,7 @@ @Service +@Log4j2 public class JwtService { @Getter @@ -68,12 +70,16 @@ public Claims getPayload(String token) { .parseSignedClaims(token) .getPayload(); } catch (SignatureException e) { + log.error("Invalid signature", e); throw AuthenticationErrorCode.INVALID_SIGNATURE.toException(); } catch (ExpiredJwtException e) { + log.error("Expired token", e); throw AuthenticationErrorCode.EXPIRED_TOKEN.toException(); } catch (MalformedJwtException e) { + log.error("Invalid token format", e); throw AuthenticationErrorCode.NOT_MATCH_TOKEN_FORMAT.toException(); } catch (Exception e) { + log.error("Failed to parse token", e); throw AuthenticationErrorCode.NOT_DEFINE_TOKEN.toException(); } } @@ -86,6 +92,7 @@ public UUID extractMemberId(String token) { public void validateTokenExpiration(String token) { Claims claims = getPayload(token); if (claims.getExpiration().before(new Date())) { + log.error("Token has expired, token: {}", token); throw AuthenticationErrorCode.EXPIRED_TOKEN.toException(); } } @@ -102,4 +109,20 @@ private String provideToken(String email, UUID id, Type type, long expiration) { .signWith(SECRET_KEY) .compact(); } + + public UUID extractMemberIdFromExpiredAccessToken(String invalidAccessToken) { + try { + Claims claims = Jwts.parser() + .verifyWith(SECRET_KEY) + .build() + .parseSignedClaims(invalidAccessToken) + .getPayload(); + return UUID.fromString(claims.get("memberId", String.class)); + } catch (ExpiredJwtException e) { + return UUID.fromString(e.getClaims().get("memberId", String.class)); + } catch (Exception e) { + log.error("Failed to extract userId from invalid token", e); + throw AuthenticationErrorCode.FAILED_TO_EXTRACT_MEMBER_ID_FROM_EXPIRED_ACCESS_TOKEN.toException(); + } + } } From 306d865a781e019e50effa2f0f216f3bdbeca1fd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:27:06 +0900 Subject: [PATCH 432/478] =?UTF-8?q?refactor:=20(#110)=20refreshToken?= =?UTF-8?q?=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=AC=20=EB=95=8C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/RefreshTokenService.java | 1 + .../auth/application/RotateAccessTokenService.java | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index a286e9c7d..4866647c7 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -38,6 +38,7 @@ public String getRefreshToken(UUID memberId) { throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); } + jwtService.getPayload(refreshToken); return refreshToken; } diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index d2e518323..e28600cf0 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -1,7 +1,7 @@ package spring.backend.auth.application; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.auth.exception.AuthenticationErrorCode; @@ -15,13 +15,15 @@ @Service @RequiredArgsConstructor -@Slf4j +@Log4j2 public class RotateAccessTokenService { private final MemberRepository memberRepository; private final JwtService jwtService; private final RefreshTokenService refreshTokenService; - public RotateAccessTokenResponse rotateAccessToken(String refreshToken) { + public RotateAccessTokenResponse rotateAccessToken(String invalidAccessToken) { + UUID memberIdFromInvalidAccessToken = jwtService.extractMemberIdFromExpiredAccessToken(invalidAccessToken); + String refreshToken = refreshTokenService.getRefreshToken(memberIdFromInvalidAccessToken); UUID memberId = extractMemberIdFromRefreshToken(refreshToken); validateRefreshToken(memberId, refreshToken); Member member = memberRepository.findById(memberId); From 03c38c3e745db26766ed5b1a13b6a203dc92b568 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 15:27:33 +0900 Subject: [PATCH 433/478] =?UTF-8?q?feat:=20(#110)=20logout=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/LogoutController.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/LogoutController.java b/src/main/java/spring/backend/auth/presentation/LogoutController.java index 05d99cd6e..83467fffb 100644 --- a/src/main/java/spring/backend/auth/presentation/LogoutController.java +++ b/src/main/java/spring/backend/auth/presentation/LogoutController.java @@ -2,10 +2,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RefreshTokenService; +import spring.backend.core.application.JwtService; + +import java.util.UUID; import spring.backend.auth.presentation.swagger.LogoutSwagger; import spring.backend.core.configuration.argumentresolver.AuthorizedMember; import spring.backend.core.configuration.interceptor.Authorization; @@ -13,14 +20,33 @@ @RestController @RequiredArgsConstructor -public class LogoutController implements LogoutSwagger { - +public class LogoutController { private final RefreshTokenService refreshTokenService; + private final JwtService jwtService; @PostMapping("/v1/logout") - @Authorization - @ResponseStatus(HttpStatus.OK) - public void logout(@AuthorizedMember Member member) { - refreshTokenService.deleteRefreshToken(member.getId()); + public ResponseEntity logout( + @CookieValue(name = "access_token", required = false) String accessToken + ) { + + UUID memberId = jwtService.extractMemberIdFromExpiredAccessToken(accessToken); + refreshTokenService.deleteRefreshToken(memberId); + + ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", "") + .httpOnly(true) + .path("/") + .maxAge(0) + .build(); + + ResponseCookie userRoleCookie = ResponseCookie.from("user_role", "") + .httpOnly(true) + .path("/") + .maxAge(0) + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) + .header(HttpHeaders.SET_COOKIE, userRoleCookie.toString()) + .build(); } } From b3bfe302d1fb679a2bb2fd4b22802a6819184b66 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 17:09:15 +0900 Subject: [PATCH 434/478] =?UTF-8?q?feat:=20(#110)=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9A=B0=ED=9A=8C=20uri=EB=A5=BC=20=EC=84=A4=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interceptor/AuthorizationInterceptor.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java index 68e8d635f..b5d8ec2b3 100644 --- a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java @@ -10,6 +10,9 @@ import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.application.JwtService; +import java.util.Arrays; +import java.util.List; + @Component @RequiredArgsConstructor @Log4j2 @@ -17,12 +20,17 @@ public class AuthorizationInterceptor implements HandlerInterceptor { private final JwtService jwtService; + private static final List PASS_THROUGH_PATTERNS = Arrays.asList( + "/swagger-ui", "/v3/api-docs", "/v1/oauth", "/v1/token/rotate" + ); + + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - if (isOAuthLoginRequest(request)) { + + if (isPassThroughRequest(request.getRequestURI())) { return true; } - String accessToken = extractToken(request); if (accessToken == null) { log.error("쿠키에 토큰이 존재하지 않습니다."); @@ -32,14 +40,15 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return true; } - private boolean isOAuthLoginRequest(HttpServletRequest request) { - return request.getRequestURI().startsWith("/v1/oauth"); + private boolean isPassThroughRequest(String uri) { + return PASS_THROUGH_PATTERNS.stream().anyMatch(uri::contains); } private String extractToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { + log.info("cookie: {}", cookie); if ("access_token".equals(cookie.getName())) { return cookie.getValue(); } From 69484bcdba824f5e4e23cb24f9babf113acbd838 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 17:09:51 +0900 Subject: [PATCH 435/478] =?UTF-8?q?feat:=20(#110)=20Swagger=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20cookie=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerConfiguration.java | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java index 176f2576b..a4e78879f 100644 --- a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java @@ -9,12 +9,12 @@ import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.security.SecurityScheme.In; -import io.swagger.v3.oas.models.security.SecurityScheme.Type; +import io.swagger.v3.oas.models.parameters.Parameter; import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,20 +26,32 @@ import java.util.stream.Collectors; @Configuration -@OpenAPIDefinition(info = @Info(title = "조각조각 API", description = "조각조각 : API 명세서", version = "v1.0.0"), - servers = {@Server(url = "${springdoc.server-url}", description = "Https Server URL")}) +@OpenAPIDefinition( + info = @Info(title = "조각조각 API", description = "조각조각 : API 명세서", version = "v1.0.0"), + servers = {@Server(url = "${springdoc.server-url}", description = "Https Server URL")} +) public class SwaggerConfiguration { @Bean - public OpenAPI openAPI(){ - SecurityScheme securityScheme = new SecurityScheme() - .type(Type.HTTP).scheme("bearer").bearerFormat("JWT") - .in(In.HEADER).name("Authorization"); - SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth"); + public OpenAPI openAPI() { + SecurityScheme cookieAuth = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.COOKIE) + .name("access_token"); + + SecurityRequirement securityRequirement = new SecurityRequirement().addList("cookieAuth"); + + Parameter accessTokenParam = new Parameter() + .in("cookie") + .name("access_token") + .schema(new StringSchema()) + .required(false); return new OpenAPI() - .components(new Components().addSecuritySchemes("bearerAuth", securityScheme)) - .security(Arrays.asList(securityRequirement)); + .components(new Components() + .addSecuritySchemes("cookieAuth", cookieAuth) + .addParameters("accessToken", accessTokenParam)) + .security(Arrays.asList(securityRequirement)); } @Bean @@ -60,25 +72,25 @@ private void generateErrorCodeResponseExample(Operation operation, Class type : types) { BaseErrorCode[] errorCodes = type.getEnumConstants(); Arrays.stream(errorCodes).map( - baseErrorCode -> ExampleHolder.builder() - .holder(getSwaggerExample(baseErrorCode)) - .code(baseErrorCode.getHttpStatus().value()) - .name(baseErrorCode.name()) - .build() + baseErrorCode -> ExampleHolder.builder() + .holder(getSwaggerExample(baseErrorCode)) + .code(baseErrorCode.getHttpStatus().value()) + .name(baseErrorCode.name()) + .build() ).forEach(exampleHolders::add); } Map> statusWithExampleHolders = new HashMap<>( - exampleHolders.stream() - .collect(Collectors.groupingBy(ExampleHolder::getCode))); + exampleHolders.stream() + .collect(Collectors.groupingBy(ExampleHolder::getCode))); addExamplesToResponses(responses, statusWithExampleHolders); } private Example getSwaggerExample(BaseErrorCode baseErrorCode) { ErrorResponse errorResponse = ErrorResponse.createSwaggerErrorResponse() - .baseErrorCode(baseErrorCode) - .build(); + .baseErrorCode(baseErrorCode) + .build(); Example example = new Example(); example.setValue(errorResponse); return example; @@ -86,16 +98,16 @@ private Example getSwaggerExample(BaseErrorCode baseErrorCode) { private void addExamplesToResponses(ApiResponses responses, Map> statusWithExampleHolders) { statusWithExampleHolders.forEach( - (status, value) -> { - Content content = new Content(); - MediaType mediaType = new MediaType(); - ApiResponse apiResponse = new ApiResponse(); - value.forEach(exampleHolder -> mediaType.addExamples(exampleHolder.getName(), - exampleHolder.getHolder())); - content.addMediaType("application/json", mediaType); - apiResponse.setContent(content); - responses.addApiResponse(status.toString(), apiResponse); - } + (status, value) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse apiResponse = new ApiResponse(); + value.forEach(exampleHolder -> mediaType.addExamples(exampleHolder.getName(), + exampleHolder.getHolder())); + content.addMediaType("application/json", mediaType); + apiResponse.setContent(content); + responses.addApiResponse(status.toString(), apiResponse); + } ); } -} \ No newline at end of file +} From fbf0dc1ccc3a62a0fd52aa6271cd9d4d944c6b15 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 22 Nov 2024 17:10:34 +0900 Subject: [PATCH 436/478] =?UTF-8?q?feat:=20(#110)=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89,=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90=20Swa?= =?UTF-8?q?gger=EB=A5=BC=20=EC=84=A4=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/LogoutController.java | 3 ++- .../RotateAccessTokenController.java | 4 ++- .../presentation/swagger/LogoutSwagger.java | 10 ++++++-- .../swagger/RotateAccessTokenSwagger.java | 25 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java diff --git a/src/main/java/spring/backend/auth/presentation/LogoutController.java b/src/main/java/spring/backend/auth/presentation/LogoutController.java index 83467fffb..01d5cb7e2 100644 --- a/src/main/java/spring/backend/auth/presentation/LogoutController.java +++ b/src/main/java/spring/backend/auth/presentation/LogoutController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RefreshTokenService; +import spring.backend.auth.presentation.swagger.LogoutSwagger; import spring.backend.core.application.JwtService; import java.util.UUID; @@ -20,7 +21,7 @@ @RestController @RequiredArgsConstructor -public class LogoutController { +public class LogoutController implements LogoutSwagger { private final RefreshTokenService refreshTokenService; private final JwtService jwtService; diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index dc413d883..66ac021dd 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; +import spring.backend.auth.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.swagger.RotateAccessTokenSwagger; import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.core.presentation.RestResponse; @@ -17,7 +19,7 @@ @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor @Log4j2 -public class RotateAccessTokenController { +public class RotateAccessTokenController implements RotateAccessTokenSwagger { private final RotateAccessTokenService rotateTokenService; @PostMapping diff --git a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java index 6abfe14cb..dd5f1ef60 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; @@ -16,6 +17,11 @@ public interface LogoutSwagger { description = "사용자의 로그아웃을 진행합니다. \n\n 로그아웃 시, 사용자의 토큰이 무효화되어, 다시 로그인을 진행해야 합니다.", operationId = "/v1/logout" ) - @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class}) - void logout(@Parameter(hidden = true) Member member); + @ApiErrorCode({ + AuthenticationErrorCode.class + }) + ResponseEntity logout( + @Parameter(description = "쿠키에 있는 access_token", required = false) + String accessToken + ); } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java new file mode 100644 index 000000000..df96dfa41 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java @@ -0,0 +1,25 @@ +package spring.backend.auth.presentation.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import spring.backend.auth.dto.response.RotateAccessTokenResponse; +import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.presentation.RestResponse; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; + +@Tag(name = "Auth", description = "인증/인가") +public interface RotateAccessTokenSwagger { + @Operation( + summary = "토큰 재발급 API", + description = "Access Token이 만료된 경우, Refresh Token을 이용하여 새로운 Access Token을 발급합니다", + operationId = "/v1/token/rotate" + ) + @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class}) + ResponseEntity> rotateAccessToken( + @Parameter(description = "쿠키에 있는 만료된 access_token", required = false) + String accessToken + ); +} From f6eb82092c2cee9429d868c7d3f1ff383f26155c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 23 Nov 2024 01:20:04 +0900 Subject: [PATCH 437/478] =?UTF-8?q?refactor:=20(#110)=20Swagger-ui?= =?UTF-8?q?=EB=A1=9C=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EB=B3=B4=EB=82=BC=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=94=20Authorization=20Header=EB=A5=BC?= =?UTF-8?q?=20=ED=86=B5=ED=95=B4=20=EC=9D=B8=EC=A6=9D/=EC=9D=B8=EA=B0=80?= =?UTF-8?q?=EB=A5=BC=20=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoginMemberArgumentResolver.java | 17 ++++++++++ .../interceptor/AuthorizationInterceptor.java | 18 ++++++++++- .../swagger/SwaggerConfiguration.java | 32 ++++++------------- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java index d00ba90d4..16ccd064a 100644 --- a/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolver.java @@ -24,6 +24,10 @@ @Log4j2 public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer "; + private final JwtService jwtService; private final MemberRepository memberRepository; @@ -48,7 +52,15 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m private String extractToken(NativeWebRequest request) { HttpServletRequest httpRequest = request.getNativeRequest(HttpServletRequest.class); + if (httpRequest != null) { + if (isSwaggerRequest(httpRequest)) { + String authHeader = httpRequest.getHeader(AUTHORIZATION_HEADER); + if (authHeader != null && authHeader.startsWith(AUTHORIZATION_BEARER_PREFIX)) { + return authHeader.substring(7); + } + } + Cookie[] cookies = httpRequest.getCookies(); if (cookies != null) { return Arrays.stream(cookies) @@ -60,4 +72,9 @@ private String extractToken(NativeWebRequest request) { } return null; } + + private boolean isSwaggerRequest(HttpServletRequest request) { + String referer = request.getHeader("Referer"); + return referer != null && referer.contains("/swagger-ui"); + } } diff --git a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java index b5d8ec2b3..da7542a27 100644 --- a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java @@ -18,6 +18,10 @@ @Log4j2 public class AuthorizationInterceptor implements HandlerInterceptor { + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer "; + private final JwtService jwtService; private static final List PASS_THROUGH_PATTERNS = Arrays.asList( @@ -32,6 +36,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return true; } String accessToken = extractToken(request); + log.info("accessToken: {}", accessToken); if (accessToken == null) { log.error("쿠키에 토큰이 존재하지 않습니다."); throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); @@ -45,10 +50,16 @@ private boolean isPassThroughRequest(String uri) { } private String extractToken(HttpServletRequest request) { + if (isSwaggerRequest(request)) { + String authHeader = request.getHeader(AUTHORIZATION_HEADER); + if (authHeader != null && authHeader.startsWith(AUTHORIZATION_BEARER_PREFIX)) { + return authHeader.substring(7); + } + } + Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { - log.info("cookie: {}", cookie); if ("access_token".equals(cookie.getName())) { return cookie.getValue(); } @@ -56,4 +67,9 @@ private String extractToken(HttpServletRequest request) { } return null; } + + private boolean isSwaggerRequest(HttpServletRequest request) { + String referer = request.getHeader("Referer"); + return referer != null && referer.contains("/swagger-ui"); + } } diff --git a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java index a4e78879f..e7f9c56a4 100644 --- a/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/swagger/SwaggerConfiguration.java @@ -9,12 +9,12 @@ import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.security.SecurityScheme.In; +import io.swagger.v3.oas.models.security.SecurityScheme.Type; import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,31 +26,19 @@ import java.util.stream.Collectors; @Configuration -@OpenAPIDefinition( - info = @Info(title = "조각조각 API", description = "조각조각 : API 명세서", version = "v1.0.0"), - servers = {@Server(url = "${springdoc.server-url}", description = "Https Server URL")} -) +@OpenAPIDefinition(info = @Info(title = "조각조각 API", description = "조각조각 : API 명세서", version = "v1.0.0"), + servers = {@Server(url = "${springdoc.server-url}", description = "Https Server URL")}) public class SwaggerConfiguration { @Bean - public OpenAPI openAPI() { - SecurityScheme cookieAuth = new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .in(SecurityScheme.In.COOKIE) - .name("access_token"); - - SecurityRequirement securityRequirement = new SecurityRequirement().addList("cookieAuth"); - - Parameter accessTokenParam = new Parameter() - .in("cookie") - .name("access_token") - .schema(new StringSchema()) - .required(false); + public OpenAPI openAPI(){ + SecurityScheme securityScheme = new SecurityScheme() + .type(Type.HTTP).scheme("bearer").bearerFormat("JWT") + .in(In.HEADER).name("Authorization"); + SecurityRequirement securityRequirement = new SecurityRequirement().addList("bearerAuth"); return new OpenAPI() - .components(new Components() - .addSecuritySchemes("cookieAuth", cookieAuth) - .addParameters("accessToken", accessTokenParam)) + .components(new Components().addSecuritySchemes("bearerAuth", securityScheme)) .security(Arrays.asList(securityRequirement)); } From f92e8d0bcbff55241674c7543cdda7676db30125 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:15:50 +0900 Subject: [PATCH 438/478] =?UTF-8?q?refactor:=20(#110)=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20LoginResponse?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/dto/response/LoginResponse.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/java/spring/backend/auth/dto/response/LoginResponse.java diff --git a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java b/src/main/java/spring/backend/auth/dto/response/LoginResponse.java deleted file mode 100644 index 80e22f1fb..000000000 --- a/src/main/java/spring/backend/auth/dto/response/LoginResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package spring.backend.auth.dto.response; - -import spring.backend.member.domain.value.Role; - -public record LoginResponse(String accessToken, Role role) { -} From c54579d4f265cf956ba8438268b071befbb2ae11 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:17:27 +0900 Subject: [PATCH 439/478] =?UTF-8?q?refactor:=20(#110)=20http://localhost:5?= =?UTF-8?q?173=EB=A5=BC=20cors=20=ED=97=88=EC=9A=A9=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/configuration/WebMvcConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index 17317f28f..d7f800911 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -25,7 +25,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://cnergy.kro.kr", "https://cnergy.p-e.kr") + .allowedOrigins("http://localhost:3000", "https://cnergy.kro.kr", "https://cnergy.p-e.kr", "http://localhost:5173") .allowedMethods("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3000); From a2564e0ee326961fcc0aa2b03db90defb98ab54d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:18:34 +0900 Subject: [PATCH 440/478] =?UTF-8?q?refactor:=20(#110)=20Dao=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=EA=B0=92=EC=9D=84=20=EB=9E=98=ED=8D=BC=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EC=9B=90?= =?UTF-8?q?=EC=8B=9C=ED=98=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ActivityWithTitleAndSavedTimeResponse.java | 2 +- .../infrastructure/persistence/jpa/dao/ActivityJpaDao.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java index 84de2d5c7..a04c3574f 100644 --- a/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java +++ b/src/main/java/spring/backend/activity/dto/response/ActivityWithTitleAndSavedTimeResponse.java @@ -9,7 +9,7 @@ public record ActivityWithTitleAndSavedTimeResponse( String title, @Schema(description = "모은 시간", example = "60") - int savedTime, + long savedTime, @Schema(description = "활동 날짜", example = "2021-07-01T00:00:00") LocalDateTime dateOfActivity diff --git a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java index bfe84a2b0..65b79d208 100644 --- a/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java +++ b/src/main/java/spring/backend/activity/infrastructure/persistence/jpa/dao/ActivityJpaDao.java @@ -98,7 +98,7 @@ and function('month', a.createdAt) = :month @Query(""" select new spring.backend.activity.dto.response.ActivityWithTitleAndSavedTimeResponse( a.title, - a.savedTime, + coalesce(sum(a.savedTime), 0), a.createdAt ) from ActivityJpaEntity a From c98413914dc07706ac2529df20d83f276651b1bc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:21:54 +0900 Subject: [PATCH 441/478] =?UTF-8?q?refactor:=20(#110)=20refreshToken=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20claim=20=EC=86=8D=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/application/JwtService.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index b12ad3daa..5e7247e78 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -98,31 +98,23 @@ public void validateTokenExpiration(String token) { } private String provideToken(String email, UUID id, Type type, long expiration) { - Date expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); + Date expiryDate; + Map claims; + if (type.equals(Type.ACCESS)) { + expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS)); + claims = Map.of( + "memberId", id.toString(), + "email", email, + "type", type.getType()); + } else { + expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); + claims = Map.of(); + } return Jwts.builder() - .claims(Map.of( - "memberId", id.toString(), - "email", email, - "type", type.getType())) + .claims(claims) .issuedAt(new Date()) .expiration(expiryDate) .signWith(SECRET_KEY) .compact(); } - - public UUID extractMemberIdFromExpiredAccessToken(String invalidAccessToken) { - try { - Claims claims = Jwts.parser() - .verifyWith(SECRET_KEY) - .build() - .parseSignedClaims(invalidAccessToken) - .getPayload(); - return UUID.fromString(claims.get("memberId", String.class)); - } catch (ExpiredJwtException e) { - return UUID.fromString(e.getClaims().get("memberId", String.class)); - } catch (Exception e) { - log.error("Failed to extract userId from invalid token", e); - throw AuthenticationErrorCode.FAILED_TO_EXTRACT_MEMBER_ID_FROM_EXPIRED_ACCESS_TOKEN.toException(); - } - } } From 1bb5cede505877f949d92dd1b0e0168c6b372aeb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:23:19 +0900 Subject: [PATCH 442/478] =?UTF-8?q?refactor:=20(#110)=20refreshToken=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=EC=97=90=20refreshToken:memberId=EC=9D=98=20?= =?UTF-8?q?=ED=98=95=ED=83=9C=EB=A1=9C=20=EC=A0=80=EC=9E=A5=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/RefreshTokenService.java | 26 +++++++------------ .../repository/RefreshTokenRepository.java | 6 ++--- .../RefreshTokenRedisRepository.java | 13 +++++----- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 4866647c7..3929cc739 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -9,7 +9,6 @@ import spring.backend.member.domain.entity.Member; import java.time.temporal.ChronoUnit; -import java.util.UUID; import java.util.concurrent.TimeUnit; @Slf4j @@ -26,30 +25,25 @@ public RefreshTokenService(JwtService jwtService, @Value("${jwt.refresh-token-ex } public String saveRefreshToken(Member member) { - refreshTokenRepository.save(member.getId(), jwtService.provideRefreshToken(member), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); - return getRefreshToken(member.getId()); + String refreshToken = jwtService.provideRefreshToken(member); + refreshTokenRepository.save(refreshToken, member.getId(), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); + return refreshToken; } - public String getRefreshToken(UUID memberId) { - String refreshToken = refreshTokenRepository.findByMemberId(memberId); - - if (refreshToken == null || refreshToken.isEmpty()) { - log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); + public void validateRefreshToken(String refreshToken) { + String savedRefreshToken = refreshTokenRepository.findByRefreshToken(refreshToken); + if (savedRefreshToken == null || savedRefreshToken.isEmpty()) { throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); } - jwtService.getPayload(refreshToken); - return refreshToken; } - public void deleteRefreshToken(UUID memberId) { - - if (refreshTokenRepository.findByMemberId(memberId) == null) { - log.error("memberId에 해당하는 리프레시 토큰이 저장소에 존재하지 않습니다."); + public void deleteRefreshToken(String refreshToken) { + if (refreshTokenRepository.findByRefreshToken(refreshToken) == null) { + log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); } - - refreshTokenRepository.deleteByMemberId(memberId); + refreshTokenRepository.deleteByRefreshToken(refreshToken); } private TimeUnit convertChronoUnitToTimeUnit(ChronoUnit chronoUnit) { diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index 508c5a42c..8add28b97 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -4,7 +4,7 @@ import java.util.concurrent.TimeUnit; public interface RefreshTokenRepository { - void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit); - String findByMemberId(UUID memberId); - void deleteByMemberId(UUID memberId); + void save(String refreshToken,UUID memberId, Long expireTime, TimeUnit timeUnit); + String findByRefreshToken(String refreshToken); + void deleteByRefreshToken(String refreshToken); } diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index 77dce1cc4..b4f93bd98 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -19,10 +19,10 @@ public class RefreshTokenRedisRepository implements RefreshTokenRepository { private final RedisTemplate redisTemplate; @Override - public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit timeUnit) { + public void save(String refreshToken, UUID memberId, Long expireTime, TimeUnit timeUnit) { try { ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(memberId.toString(), refreshToken, expireTime, timeUnit); + valueOperations.set(refreshToken, memberId.toString(), expireTime, timeUnit); } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); @@ -32,23 +32,22 @@ public void save(UUID memberId, String refreshToken, Long expireTime, TimeUnit t } @Override - public String findByMemberId(UUID memberId) { + public String findByRefreshToken(String refreshToken) { try { ValueOperations valueOperations = redisTemplate.opsForValue(); - return valueOperations.get(memberId.toString()); + return valueOperations.get(refreshToken); } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); } catch (Exception e) { throw GlobalErrorCode.INTERNAL_ERROR.toException(); } - } @Override - public void deleteByMemberId(UUID memberId) { + public void deleteByRefreshToken(String refreshToken) { try { - redisTemplate.delete(memberId.toString()); + redisTemplate.delete(refreshToken); } catch (RedisConnectionException e) { log.error("Redis 연결 오류 : {}", e.getMessage()); throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); From b7ddd0c361c74e9e3a67f572bcba67c1f18c3029 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:25:11 +0900 Subject: [PATCH 443/478] =?UTF-8?q?refactor:=20(#110)=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/RotateAccessTokenService.java | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index e28600cf0..db14e77a2 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -3,8 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; -import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; +import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.core.application.JwtService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; @@ -20,29 +21,15 @@ public class RotateAccessTokenService { private final MemberRepository memberRepository; private final JwtService jwtService; private final RefreshTokenService refreshTokenService; + private final RefreshTokenRedisRepository refreshTokenRedisRepository; - public RotateAccessTokenResponse rotateAccessToken(String invalidAccessToken) { - UUID memberIdFromInvalidAccessToken = jwtService.extractMemberIdFromExpiredAccessToken(invalidAccessToken); - String refreshToken = refreshTokenService.getRefreshToken(memberIdFromInvalidAccessToken); - UUID memberId = extractMemberIdFromRefreshToken(refreshToken); - validateRefreshToken(memberId, refreshToken); - Member member = memberRepository.findById(memberId); - return new RotateAccessTokenResponse(jwtService.provideAccessToken(Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException))); - } - - private UUID extractMemberIdFromRefreshToken(String refreshToken) { - if (refreshToken == null) { - log.error("쿠키에 refreshToken이 존재하지 않습니다."); + public RotateAccessTokenResponse rotateAccessToken(String refreshToken) { + if(refreshToken == null) { throw AuthenticationErrorCode.MISSING_COOKIE_VALUE.toException(); } - return UUID.fromString(jwtService.getPayload(refreshToken).get("memberId", String.class)); - } - - private void validateRefreshToken(UUID memberId, String refreshToken) { - String savedRefreshToken = refreshTokenService.getRefreshToken(memberId); - if (!savedRefreshToken.equals(refreshToken)) { - log.error("리프레시 토큰이 저장소에 존재하지 않습니다."); - throw AuthenticationErrorCode.NOT_EXIST_REFRESH_TOKEN.toException(); - } + refreshTokenService.validateRefreshToken(refreshToken); + UUID memberId = UUID.fromString(refreshTokenRedisRepository.findByRefreshToken(refreshToken)); + Member member = memberRepository.findById(memberId); + return new RotateAccessTokenResponse(jwtService.provideAccessToken(Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException))); } } From 413b112fec1c7cb801aaf0a6885cbb5f1e5f44e1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:25:53 +0900 Subject: [PATCH 444/478] =?UTF-8?q?refactor:=20(#110)=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=97=90=20=EC=9E=88=EB=8A=94=20refreshToken=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B3=A0,=20Swagger=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RotateAccessTokenController.java | 15 ++++++++------- .../swagger/RotateAccessTokenSwagger.java | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 66ac021dd..d83a78c46 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -10,11 +10,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; -import spring.backend.auth.dto.response.RotateAccessTokenResponse; -import spring.backend.auth.presentation.swagger.RotateAccessTokenSwagger; import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.swagger.RotateAccessTokenSwagger; import spring.backend.core.presentation.RestResponse; +import static org.springframework.http.ResponseCookie.from; + @RestController @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor @@ -24,16 +25,16 @@ public class RotateAccessTokenController implements RotateAccessTokenSwagger { @PostMapping public ResponseEntity> rotateAccessToken( - @CookieValue(name = "access_token", required = false) String accessToken + @CookieValue(name = "refresh_token", required = false) String refreshToken ) { - RotateAccessTokenResponse rotateAccessTokenResponse = rotateTokenService.rotateAccessToken(accessToken); - ResponseCookie cookie = ResponseCookie.from("access_token", rotateAccessTokenResponse.accessToken()) - .httpOnly(true) + RotateAccessTokenResponse rotateAccessTokenResponse = rotateTokenService.rotateAccessToken(refreshToken); + ResponseCookie newAccessToken = from("access_token", rotateAccessTokenResponse.accessToken()) + .httpOnly(false) .path("/") .build(); return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .header(HttpHeaders.SET_COOKIE, newAccessToken.toString()) .build(); } } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java index df96dfa41..6ab8c399f 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.auth.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.presentation.RestResponse; import spring.backend.auth.exception.AuthenticationErrorCode; @@ -19,7 +19,7 @@ public interface RotateAccessTokenSwagger { ) @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class}) ResponseEntity> rotateAccessToken( - @Parameter(description = "쿠키에 있는 만료된 access_token", required = false) - String accessToken + @Parameter(description = "쿠키에 있는 refresh_token", required = false) + String refreshToken ); } From 2659556860d9093526a168c6560abcdf5a50744b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:26:33 +0900 Subject: [PATCH 445/478] =?UTF-8?q?refactor:=20(#110)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4=EC=97=90=20AccessTok?= =?UTF-8?q?en=EA=B3=BC=20RefreshToken=EC=9D=84=20=EB=84=A3=EB=8A=94?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/HandleOAuthLoginService.java | 12 +++++------ .../HandleOAuthLoginController.java | 20 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index ce95f48a3..1b61478c2 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -3,17 +3,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; -import spring.backend.auth.presentation.dto.response.LoginResponse; -import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; -import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.OAuthRestClient; import spring.backend.auth.infrastructure.OAuthRestClientFactory; -import spring.backend.member.domain.service.CreateMemberWithOAuthService; +import spring.backend.auth.presentation.dto.response.LoginResponse; +import spring.backend.auth.presentation.dto.response.OAuthAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.OAuthResourceResponse; +import spring.backend.core.application.JwtService; import spring.backend.member.domain.entity.Member; +import spring.backend.member.domain.service.CreateMemberWithOAuthService; import spring.backend.member.domain.value.Provider; import spring.backend.member.presentation.dto.request.CreateMemberWithOAuthRequest; -import spring.backend.core.application.JwtService; @Service @RequiredArgsConstructor @@ -54,6 +54,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); refreshTokenService.saveRefreshToken(member); - return new LoginResponse(jwtService.provideAccessToken(member), member.getRole()); + return LoginResponse.of(jwtService.provideAccessToken(member), jwtService.provideRefreshToken(member), member); } } diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index 066137968..ff4e3c8f7 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -8,7 +8,6 @@ import spring.backend.auth.application.HandleOAuthLoginService; import spring.backend.auth.presentation.dto.response.LoginResponse; import spring.backend.core.presentation.RestResponse; -import spring.backend.auth.dto.response.LoginResponse; @RestController @RequestMapping("/v1/oauth/login") @@ -21,17 +20,22 @@ public class HandleOAuthLoginController { public ResponseEntity handleOAuthLogin(@RequestParam(value = "code", required = false) String code, @RequestParam(value = "state", required = false) String state, @PathVariable String providerName) { LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); + // Todo: 배포 시 httpOnly(true)로 변경 ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) - .httpOnly(true) + .httpOnly(false) + .path("/") .build(); - - ResponseCookie roleCookie = ResponseCookie.from("user_role", loginResponse.role().toString()) - .httpOnly(true) + // Todo: 배포 시 httpOnly(true)로 변경 + ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", loginResponse.refreshToken()) + .httpOnly(false) + .path("/") .build(); return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) - .header(HttpHeaders.SET_COOKIE, roleCookie.toString()) - .build(); + .headers(header -> { + header.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); + header.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + }) + .body(new RestResponse<>(loginResponse.userInfo())); } } From 132d3332eb4700e5e5a173a9ec5580119b220b4c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:27:07 +0900 Subject: [PATCH 446/478] =?UTF-8?q?refactor:=20(#110)=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=8B=9C=20=EC=BF=A0=ED=82=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=ED=95=98=EA=B3=A0,=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A0=80=EC=9E=A5=EC=86=8C=EC=97=90=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20RefreshToken=EC=9D=84=20=EC=82=AD=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/LogoutController.java | 22 +++++-------------- .../presentation/swagger/LogoutSwagger.java | 6 ++--- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/LogoutController.java b/src/main/java/spring/backend/auth/presentation/LogoutController.java index 01d5cb7e2..ae790e6f4 100644 --- a/src/main/java/spring/backend/auth/presentation/LogoutController.java +++ b/src/main/java/spring/backend/auth/presentation/LogoutController.java @@ -1,24 +1,16 @@ package spring.backend.auth.presentation; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RefreshTokenService; import spring.backend.auth.presentation.swagger.LogoutSwagger; import spring.backend.core.application.JwtService; -import java.util.UUID; -import spring.backend.auth.presentation.swagger.LogoutSwagger; -import spring.backend.core.configuration.argumentresolver.AuthorizedMember; -import spring.backend.core.configuration.interceptor.Authorization; -import spring.backend.member.domain.entity.Member; - @RestController @RequiredArgsConstructor public class LogoutController implements LogoutSwagger { @@ -27,27 +19,23 @@ public class LogoutController implements LogoutSwagger { @PostMapping("/v1/logout") public ResponseEntity logout( - @CookieValue(name = "access_token", required = false) String accessToken + @CookieValue(name = "access_token", required = false) String accessToken, + @CookieValue(name = "refresh_token", required = false) String refreshToken ) { - - UUID memberId = jwtService.extractMemberIdFromExpiredAccessToken(accessToken); - refreshTokenService.deleteRefreshToken(memberId); - + refreshTokenService.deleteRefreshToken(refreshToken); ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", "") .httpOnly(true) .path("/") .maxAge(0) .build(); - - ResponseCookie userRoleCookie = ResponseCookie.from("user_role", "") + ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", "") .httpOnly(true) .path("/") .maxAge(0) .build(); - return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()) - .header(HttpHeaders.SET_COOKIE, userRoleCookie.toString()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) .build(); } } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java index dd5f1ef60..973e8d965 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java @@ -6,8 +6,6 @@ import org.springframework.http.ResponseEntity; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; -import spring.backend.core.exception.error.GlobalErrorCode; -import spring.backend.member.domain.entity.Member; @Tag(name = "Auth", description = "인증/인가") public interface LogoutSwagger { @@ -22,6 +20,8 @@ public interface LogoutSwagger { }) ResponseEntity logout( @Parameter(description = "쿠키에 있는 access_token", required = false) - String accessToken + String accessToken, + @Parameter(description = "쿠키에 있는 refresh_token", required = false) + String refreshToken ); } From 4986ebbe9eba68517b7d52711e306879a978e30e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:28:47 +0900 Subject: [PATCH 447/478] =?UTF-8?q?refactor:=20(#110)=20RefreshTokenServic?= =?UTF-8?q?eTest=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/RefreshTokenServiceTest.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java index 8f37d21fe..c08f12243 100644 --- a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java @@ -1,6 +1,9 @@ package spring.backend.core.application; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -8,19 +11,21 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import spring.backend.auth.application.RefreshTokenService; -import spring.backend.core.exception.DomainException; +import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; import spring.backend.member.domain.entity.Member; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @SpringBootTest public class RefreshTokenServiceTest { @Autowired private RefreshTokenService refreshTokenService; + @Autowired + private RefreshTokenRedisRepository refreshTokenRedisRepository; + private final UUID memberId = UUID.randomUUID(); private final Member member = Member.builder() @@ -47,18 +52,12 @@ static void afterAll(@Qualifier("redisConnectionFactory") LettuceConnectionFacto connectionFactory.getConnection().flushDb(); } - @DisplayName("RefreshToken이 발급될 때 ID와 RefreshToken를 Redis에 저장된다") + @DisplayName("RefreshToken이 발급될 때 RefreshToken과 ID를 Redis에 저장된다") @Test void saveRefreshTokenWhenTokenReleased() { - // when & then - assertThat(refreshTokenService.saveRefreshToken(member)).isEqualTo(refreshTokenService.getRefreshToken(memberId)); - } - - @DisplayName("memberId에 해당하는 RefreshToken이 Redis에 저장되어 있지 않은 경우 에러를 반환한다.") - @Test - void throwExceptionWhenRefreshTokenIsNotInRedis() { - // when & then - assertThatThrownBy(() -> refreshTokenService.getRefreshToken(UUID.randomUUID())) - .isInstanceOf(DomainException.class).hasMessage("리프레시 토큰이 저장소에 존재하지 않습니다."); + // when + String refreshToken = refreshTokenService.saveRefreshToken(member); + // then + assertThat(member.getId().toString()).isEqualTo(refreshTokenRedisRepository.findByRefreshToken(refreshToken)); } -} \ No newline at end of file +} From 1b862bc6a0feb2c1e3457a02d91e3f6a044d5940 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:29:16 +0900 Subject: [PATCH 448/478] =?UTF-8?q?refactor:=20(#110)=20LoginMemberArgumen?= =?UTF-8?q?tResolverTest=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LoginMemberArgumentResolverTest.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java index 23fc9681f..6a80caf6c 100644 --- a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java @@ -1,5 +1,7 @@ package spring.backend.core.configuration.argumentresolver; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,6 +10,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import spring.backend.core.application.JwtService; @@ -50,7 +53,8 @@ public void setUp() { member = Member.builder() .id(memberId) .build(); - token = jwtService.provideAccessToken(member); + token = "mockToken"; + when(jwtService.provideAccessToken(any(Member.class))).thenReturn(token); } @DisplayName("LoginMember 어노테이션이 있는 경우 지원한다") @@ -61,19 +65,43 @@ public void supportsParameterReturnsTrueForLoginMember() { Assertions.assertTrue(loginMemberArgumentResolver.supportsParameter(parameter)); } - @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 Member 객체를 반환한다") +// @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 Member 객체를 반환한다") +// @Test +// public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Exception { +// // when +// MethodParameter parameter = mock(MethodParameter.class); +// when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); +// when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); +// when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); +// when(memberRepository.findById(memberId)).thenReturn(member); +// +// // then +// Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); +// assertNotNull(result); +// assertThat(result).isEqualTo(member); +// } + + @DisplayName("쿠키에 유효한 토큰이 있을 때 Member 객체를 반환한다") @Test - public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Exception { + public void returnsMemberObject_whenValidTokenInCookie() throws Exception { + // given + String cookieName = "access_token"; + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + System.out.println("token: " + token); + System.out.println("member : " + member); + mockRequest.setCookies(new Cookie(cookieName, token)); + // when MethodParameter parameter = mock(MethodParameter.class); when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); - when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); - when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); + when(webRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(mockRequest); + when(jwtService.extractMemberId(token)).thenReturn(memberId); when(memberRepository.findById(memberId)).thenReturn(member); // then Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); assertNotNull(result); - assertThat(result).isEqualTo(member); + assertThat(result).isInstanceOf(Member.class); + assertThat(((Member) result).getId()).isEqualTo(memberId); } } From 6eb71af3978f7384bec00c6d74d899a7c24834b8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:30:35 +0900 Subject: [PATCH 449/478] =?UTF-8?q?chore:=20(#110)=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20pr=20=EC=8B=9C=20ci/c?= =?UTF-8?q?d=EA=B0=80=20=EB=8F=8C=EC=95=84=EA=B0=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 34ede148d..6f7d454db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Cnergy Backend CI/CD on: push: branches: [dev] + pull_request: + branches: [dev] jobs: ci: From 31fb9c6d7310188832d245dbb5788e53757a12fe Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 13 Jan 2025 13:51:21 +0900 Subject: [PATCH 450/478] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=EC=97=90=20ht?= =?UTF-8?q?tpOnly=20=EC=84=A4=EC=A0=95=EC=9D=84=20true=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/exception/AuthenticationErrorCode.java | 2 +- .../backend/auth/presentation/HandleOAuthLoginController.java | 4 ++-- .../auth/presentation/RotateAccessTokenController.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 578acc693..d6c905060 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -24,7 +24,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { RESOURCE_SERVER_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "OAuth Resource Server에 접근할 수 없습니다."), UNSUPPORTED_REDIS_TIME_TYPE(HttpStatus.BAD_REQUEST, "Redis 만료시간은 ChronoUnit 타입이어야 합니다."), MISMATCH_TOKEN_MEMBER(HttpStatus.UNAUTHORIZED, "토큰의 회원 ID와 요청한 회원 ID가 일치하지 않습니다."), - NOT_EXIST_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "리프레시 토큰이 저장소에 존재하지 않습니다."), + NOT_EXIST_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 저장소에 존재하지 않습니다."), MISSING_COOKIE_VALUE(HttpStatus.BAD_REQUEST, "쿠키값이 존재하지 않습니다."), INVALID_MEMBER_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입을 위한 사용자 조건이 유효하지 않습니다."), NOT_EXIST_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입 요청이 유효하지 않습니다."), diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index ff4e3c8f7..027a7d15c 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -22,12 +22,12 @@ public ResponseEntity handleOAuthLogin(@RequestParam(value = "code", required LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); // Todo: 배포 시 httpOnly(true)로 변경 ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) - .httpOnly(false) + .httpOnly(true) .path("/") .build(); // Todo: 배포 시 httpOnly(true)로 변경 ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", loginResponse.refreshToken()) - .httpOnly(false) + .httpOnly(true) .path("/") .build(); diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index d83a78c46..8ca02e7c9 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -29,7 +29,7 @@ public ResponseEntity> rotateAccessToken ) { RotateAccessTokenResponse rotateAccessTokenResponse = rotateTokenService.rotateAccessToken(refreshToken); ResponseCookie newAccessToken = from("access_token", rotateAccessTokenResponse.accessToken()) - .httpOnly(false) + .httpOnly(true) .path("/") .build(); From d326445f7e35132509712723b38dffa1493c3a7b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:53:46 +0900 Subject: [PATCH 451/478] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EC=9D=98=20pull=5Frequests=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=B0=ED=8F=AC=EB=90=98=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6f7d454db..34ede148d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Cnergy Backend CI/CD on: push: branches: [dev] - pull_request: - branches: [dev] jobs: ci: From 0417d3f55a12b7b8176fd64cfa2aa7f0592218a3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:54:24 +0900 Subject: [PATCH 452/478] =?UTF-8?q?refactor:=20AuthenticationErrorCode?= =?UTF-8?q?=EC=9D=98=20NOT=5FEXIST=5FTOKEN=EC=9D=98=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20NOT=5FEXIST=5FTOKEN=5FCOOKIE=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/exception/AuthenticationErrorCode.java | 2 +- .../configuration/interceptor/AuthorizationInterceptor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index d6c905060..02a8febcc 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -11,7 +11,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { NOT_EXIST_HEADER(HttpStatus.UNAUTHORIZED, "Authorization Header가 존재하지 않습니다."), - NOT_EXIST_TOKEN(HttpStatus.UNAUTHORIZED, "쿠키에 Token이 존재하지 않습니다."), + NOT_EXIST_TOKEN_In_COOKIE(HttpStatus.UNAUTHORIZED, "쿠키에 Token이 존재하지 않습니다."), NOT_MATCH_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "토큰의 형식이 맞지 않습니다."), INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED, "토큰의 서명이 올바르지 않습니다."), NOT_DEFINE_TOKEN(HttpStatus.UNAUTHORIZED, "정의되지 않은 토큰입니다."), diff --git a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java index da7542a27..8aa052f28 100644 --- a/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/spring/backend/core/configuration/interceptor/AuthorizationInterceptor.java @@ -39,7 +39,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons log.info("accessToken: {}", accessToken); if (accessToken == null) { log.error("쿠키에 토큰이 존재하지 않습니다."); - throw AuthenticationErrorCode.NOT_EXIST_TOKEN.toException(); + throw AuthenticationErrorCode.NOT_EXIST_TOKEN_In_COOKIE.toException(); } jwtService.validateTokenExpiration(accessToken); return true; From f415b63e68f646dfde7f9879ee910cd6c1fb1fea Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:54:57 +0900 Subject: [PATCH 453/478] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=EC=9D=84=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/HandleOAuthLoginController.java | 11 ++++++----- .../LoginMemberArgumentResolverTest.java | 16 ---------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index 027a7d15c..93fb9c979 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*; import spring.backend.auth.application.HandleOAuthLoginService; import spring.backend.auth.presentation.dto.response.LoginResponse; +import spring.backend.auth.presentation.dto.response.LoginUserInfoResponse; import spring.backend.core.presentation.RestResponse; @RestController @@ -17,25 +18,25 @@ public class HandleOAuthLoginController { private final HandleOAuthLoginService handleOAuthLoginService; @GetMapping("/{providerName}") - public ResponseEntity handleOAuthLogin(@RequestParam(value = "code", required = false) String code, - @RequestParam(value = "state", required = false) String state, @PathVariable String providerName) { + public ResponseEntity> handleOAuthLogin(@RequestParam(value = "code", required = false) String code, + @RequestParam(value = "state", required = false) String state, @PathVariable String providerName) { LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); - // Todo: 배포 시 httpOnly(true)로 변경 ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) .httpOnly(true) .path("/") .build(); - // Todo: 배포 시 httpOnly(true)로 변경 ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", loginResponse.refreshToken()) .httpOnly(true) .path("/") .build(); + LoginUserInfoResponse loginUserInfoResponse = LoginUserInfoResponse.from(loginResponse); + return ResponseEntity.ok() .headers(header -> { header.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); header.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); }) - .body(new RestResponse<>(loginResponse.userInfo())); + .body(new RestResponse<>(loginUserInfoResponse)); } } diff --git a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java index 6a80caf6c..ed5722912 100644 --- a/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java +++ b/src/test/java/spring/backend/core/configuration/argumentresolver/LoginMemberArgumentResolverTest.java @@ -65,22 +65,6 @@ public void supportsParameterReturnsTrueForLoginMember() { Assertions.assertTrue(loginMemberArgumentResolver.supportsParameter(parameter)); } -// @DisplayName("Authorization 헤더에 유효한 토큰이 있을 때 Member 객체를 반환한다") -// @Test -// public void returnsMemberObject_whenAuthorizationHeaderIsProvided() throws Exception { -// // when -// MethodParameter parameter = mock(MethodParameter.class); -// when(parameter.hasParameterAnnotation(LoginMember.class)).thenReturn(true); -// when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + token); -// when(jwtService.extractMemberId(any(String.class))).thenReturn(memberId); -// when(memberRepository.findById(memberId)).thenReturn(member); -// -// // then -// Object result = loginMemberArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, null); -// assertNotNull(result); -// assertThat(result).isEqualTo(member); -// } - @DisplayName("쿠키에 유효한 토큰이 있을 때 Member 객체를 반환한다") @Test public void returnsMemberObject_whenValidTokenInCookie() throws Exception { From c49ff65c7d4329cac93203263b7222fc4b62bcf0 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:55:08 +0900 Subject: [PATCH 454/478] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=B0=98=ED=99=98=20=EA=B0=92=EC=9D=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/application/HandleOAuthLoginService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index 1b61478c2..f888d1196 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -49,7 +49,6 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s CreateMemberWithOAuthRequest createMemberWithOAuthRequest = CreateMemberWithOAuthRequest.builder() .provider(provider) .email(oAuthResourceResponse.getEmail()) - .nickname(oAuthResourceResponse.getName()) .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); From d43eb07d861d0410ca9ecd482f58b6c0f0e3036d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:55:32 +0900 Subject: [PATCH 455/478] =?UTF-8?q?refactor:=20enum=20=EC=83=81=EC=88=98?= =?UTF-8?q?=20=EB=B9=84=EA=B5=90=EB=A5=BC=20equals()=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=3D=3D=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.(NPE=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=EC=9D=B4=EC=8A=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/spring/backend/core/application/JwtService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index 5e7247e78..f01485510 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -100,7 +100,7 @@ public void validateTokenExpiration(String token) { private String provideToken(String email, UUID id, Type type, long expiration) { Date expiryDate; Map claims; - if (type.equals(Type.ACCESS)) { + if (type == Type.ACCESS) { expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS)); claims = Map.of( "memberId", id.toString(), From 9ca5f1cac5c382eeaa96e2f46abfd6e4ec84cf0d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:59:15 +0900 Subject: [PATCH 456/478] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=EC=9D=84=20=EC=A7=80=ED=82=A8?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/domain/repository/RefreshTokenRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index 8add28b97..0e6b92b7b 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -4,7 +4,7 @@ import java.util.concurrent.TimeUnit; public interface RefreshTokenRepository { - void save(String refreshToken,UUID memberId, Long expireTime, TimeUnit timeUnit); + void save(String refreshToken, UUID memberId, Long expireTime, TimeUnit timeUnit); String findByRefreshToken(String refreshToken); void deleteByRefreshToken(String refreshToken); } From 3bedc0ef514940b12ac82e83ec2f47f30fac5ceb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 12 Feb 2025 16:59:45 +0900 Subject: [PATCH 457/478] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=A6=AC=ED=84=B4=EA=B0=92=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20Response=20Record=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/LogoutController.java | 5 +-- .../dto/response/LoginUserInfoResponse.java | 42 +++++++++++++++++++ .../presentation/swagger/LogoutSwagger.java | 6 ++- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/main/java/spring/backend/auth/presentation/dto/response/LoginUserInfoResponse.java diff --git a/src/main/java/spring/backend/auth/presentation/LogoutController.java b/src/main/java/spring/backend/auth/presentation/LogoutController.java index ae790e6f4..b28711ae6 100644 --- a/src/main/java/spring/backend/auth/presentation/LogoutController.java +++ b/src/main/java/spring/backend/auth/presentation/LogoutController.java @@ -9,16 +9,15 @@ import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RefreshTokenService; import spring.backend.auth.presentation.swagger.LogoutSwagger; -import spring.backend.core.application.JwtService; +import spring.backend.core.presentation.RestResponse; @RestController @RequiredArgsConstructor public class LogoutController implements LogoutSwagger { private final RefreshTokenService refreshTokenService; - private final JwtService jwtService; @PostMapping("/v1/logout") - public ResponseEntity logout( + public ResponseEntity> logout( @CookieValue(name = "access_token", required = false) String accessToken, @CookieValue(name = "refresh_token", required = false) String refreshToken ) { diff --git a/src/main/java/spring/backend/auth/presentation/dto/response/LoginUserInfoResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/LoginUserInfoResponse.java new file mode 100644 index 000000000..82d0f3550 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/dto/response/LoginUserInfoResponse.java @@ -0,0 +1,42 @@ +package spring.backend.auth.presentation.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import spring.backend.member.domain.value.Gender; +import spring.backend.member.domain.value.Role; + +import java.time.LocalDateTime; + +public record LoginUserInfoResponse( + @Schema(description = "사용자 유형(MEMBER, GUEST)", example = "MEMBER") + Role role, + + @Schema(description = "사용자 이메일", example = "example@example.com") + String email, + + @Schema(description = "사용자 닉네임", example = "john_doe") + String nickname, + + @Schema(description = "사용자 출생 연도", example = "1990") + int birthYear, + + @Schema(description = "사용자 성별 (MALE, FEMALE, NONE)", example = "MALE") + Gender gender, + + @Schema(description = "사용자 프로필 이미지 URL", example = "https://example.com/profile.jpg") + String profileImage, + + @Schema(description = "사용자 가입 날짜", example = "2024-11-14T06:10:55.091954") + LocalDateTime registrationDate +) { + public static LoginUserInfoResponse from(LoginResponse loginResponse) { + return new LoginUserInfoResponse( + loginResponse.userInfo().role(), + loginResponse.userInfo().email(), + loginResponse.userInfo().nickname(), + loginResponse.userInfo().birthYear(), + loginResponse.userInfo().gender(), + loginResponse.userInfo().profileImage(), + loginResponse.userInfo().registrationDate() + ); + } +} diff --git a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java index 973e8d965..11c75ec5e 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/LogoutSwagger.java @@ -6,6 +6,8 @@ import org.springframework.http.ResponseEntity; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.core.configuration.swagger.ApiErrorCode; +import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; @Tag(name = "Auth", description = "인증/인가") public interface LogoutSwagger { @@ -16,9 +18,9 @@ public interface LogoutSwagger { operationId = "/v1/logout" ) @ApiErrorCode({ - AuthenticationErrorCode.class + GlobalErrorCode.class, AuthenticationErrorCode.class }) - ResponseEntity logout( + ResponseEntity> logout( @Parameter(description = "쿠키에 있는 access_token", required = false) String accessToken, @Parameter(description = "쿠키에 있는 refresh_token", required = false) From 7e7185056bd110cf84b75566e2426cd9ef91bcf0 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:39:44 +0900 Subject: [PATCH 458/478] =?UTF-8?q?chore:=20geoip2=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + gradle/db.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6e2a09cdf..df033379a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ out/ ### Configuration ### src/main/resources/application-*.yml src/test/resources/application.yml +src/main/resources/maxmind diff --git a/gradle/db.gradle b/gradle/db.gradle index 6aa84d287..d644a3d38 100644 --- a/gradle/db.gradle +++ b/gradle/db.gradle @@ -2,6 +2,7 @@ dependencies { // DB, JPA runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.maxmind.geoip2:geoip2:4.1.0' testImplementation 'com.h2database:h2' From 21e2cbfc1d7209ceab2e7fdc3943337b4acd0d8b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:40:19 +0900 Subject: [PATCH 459/478] =?UTF-8?q?chore:=20GeoIpConfiguration=EC=9D=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/GeoIpConfiguration.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/GeoIpConfiguration.java diff --git a/src/main/java/spring/backend/core/configuration/GeoIpConfiguration.java b/src/main/java/spring/backend/core/configuration/GeoIpConfiguration.java new file mode 100644 index 000000000..cc68c664c --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/GeoIpConfiguration.java @@ -0,0 +1,21 @@ +package spring.backend.core.configuration; + +import com.maxmind.db.CHMCache; +import com.maxmind.geoip2.DatabaseReader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; + +@Configuration +public class GeoIpConfiguration { + + @Bean + public DatabaseReader databaseReader() throws IOException { + ClassPathResource resource = new ClassPathResource("maxmind/GeoLite2-City.mmdb"); + return new DatabaseReader.Builder(resource.getInputStream()) + .withCache(new CHMCache()) + .build(); + } +} From 9f9ffbff61abaccee92520179678899faef0eb68 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:40:40 +0900 Subject: [PATCH 460/478] =?UTF-8?q?feat:=20=EB=91=90=20=EC=A2=8C=ED=91=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9D=B4=20=EA=B1=B0=EB=A6=AC=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9C=A0=ED=8B=B8=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/core/util/geo/GeoUtil.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/spring/backend/core/util/geo/GeoUtil.java diff --git a/src/main/java/spring/backend/core/util/geo/GeoUtil.java b/src/main/java/spring/backend/core/util/geo/GeoUtil.java new file mode 100644 index 000000000..9038ccda7 --- /dev/null +++ b/src/main/java/spring/backend/core/util/geo/GeoUtil.java @@ -0,0 +1,22 @@ +package spring.backend.core.util.geo; + +public class GeoUtil { + private static final double EARTH_RADIUS = 6371; + + public static double calculateDistanceBetweenTwoCoordinate(double savedLat, double savedLon, double newLat, double newLon) { + double deltaLatDiff = Math.toRadians(Math.abs(newLat - savedLat)); + double deltaLonDiff = Math.toRadians(Math.abs(newLon - savedLon)); + + double sinDeltaLatDiff = Math.sin(deltaLatDiff / 2); + double sinDeltaLonDiff = Math.sin(deltaLonDiff / 2); + + double squareRoot = Math.sqrt( + sinDeltaLatDiff * sinDeltaLatDiff + + (Math.cos(Math.toRadians(savedLat)) + * Math.cos(Math.toRadians(newLat)) + * sinDeltaLonDiff * sinDeltaLonDiff) + ); + + return 2 * EARTH_RADIUS * Math.asin(squareRoot); + } +} From d07d379db5bfca741dda3209b401e763e937fe37 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:41:14 +0900 Subject: [PATCH 461/478] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=9D=98=20IP=20=ED=9A=8D?= =?UTF-8?q?=EB=93=9D=EC=9A=A9=20ArgumentResolver=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../argumentresolver/ClientIp.java | 11 +++ .../ClientIpArgumentResolver.java | 69 +++++++++++++++++++ .../util/geo/dto/response/Coordinate.java | 10 +++ 3 files changed, 90 insertions(+) create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/ClientIp.java create mode 100644 src/main/java/spring/backend/core/configuration/argumentresolver/ClientIpArgumentResolver.java create mode 100644 src/main/java/spring/backend/core/util/geo/dto/response/Coordinate.java diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIp.java b/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIp.java new file mode 100644 index 000000000..47e720434 --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIp.java @@ -0,0 +1,11 @@ +package spring.backend.core.configuration.argumentresolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ClientIp { +} diff --git a/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIpArgumentResolver.java b/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIpArgumentResolver.java new file mode 100644 index 000000000..d04f6d43a --- /dev/null +++ b/src/main/java/spring/backend/core/configuration/argumentresolver/ClientIpArgumentResolver.java @@ -0,0 +1,69 @@ +package spring.backend.core.configuration.argumentresolver; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.log4j.Log4j2; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.stream.Stream; + +@Component +@Log4j2 +public class ClientIpArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String[] IP_HEADER_CANDIDATES = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR", + "X-Real-IP" + }; + + private static final String LOCAL_HOST_IPV6 = "0:0:0:0:0:0:0:1"; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ClientIp.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = ((ServletWebRequest)webRequest).getRequest(); + return extractIpFromServlet(request); + } + + private String extractIpFromServlet(HttpServletRequest request) { + String ipList = Stream.of( IP_HEADER_CANDIDATES) + .map(request::getHeader) + .filter(header -> header != null && !header.isEmpty()) + .findFirst() + .orElseGet(request::getRemoteAddr); + String ip = getIpFromIpList(ipList); + + return ip; + } + + private String getIpFromIpList(String ipList) { + if (ipList.contains(",")) { + return Stream.of(ipList.split(",")) + .map(String::trim) + .filter(ip -> !ip.equalsIgnoreCase("unknown")) + .findFirst() + .orElse(""); + + } + + if (ipList.equals(LOCAL_HOST_IPV6)) { + return "127.0.0.1"; + } + + return ipList; + } +} + diff --git a/src/main/java/spring/backend/core/util/geo/dto/response/Coordinate.java b/src/main/java/spring/backend/core/util/geo/dto/response/Coordinate.java new file mode 100644 index 000000000..a744f31e2 --- /dev/null +++ b/src/main/java/spring/backend/core/util/geo/dto/response/Coordinate.java @@ -0,0 +1,10 @@ +package spring.backend.core.util.geo.dto.response; + +public record Coordinate( + double latitude, + double longitude +) { + public static Coordinate of(double latitude, double longitude) { + return new Coordinate(latitude, longitude); + } +} From 0125671d109717ac665f9f9d47b88ee34e748b3a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:41:27 +0900 Subject: [PATCH 462/478] =?UTF-8?q?feat:=20argumentResolver=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/configuration/WebMvcConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java index d7f800911..e8a06297d 100644 --- a/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java +++ b/src/main/java/spring/backend/core/configuration/WebMvcConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import spring.backend.core.configuration.argumentresolver.AuthorizedMemberArgumentResolver; +import spring.backend.core.configuration.argumentresolver.ClientIpArgumentResolver; import spring.backend.core.configuration.argumentresolver.LoginMemberArgumentResolver; import spring.backend.core.configuration.interceptor.AuthorizationInterceptor; @@ -22,6 +23,8 @@ public class WebMvcConfiguration implements WebMvcConfigurer { private final AuthorizedMemberArgumentResolver authorizedMemberArgumentResolver; + private final ClientIpArgumentResolver clientIpArgumentResolver; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -38,6 +41,7 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { + resolvers.add(clientIpArgumentResolver); resolvers.add(loginMemberArgumentResolver); resolvers.add(authorizedMemberArgumentResolver); } From 50bb6003230ca943dd4577c2a6be85545eb9fa7c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 15 Feb 2025 09:52:52 +0900 Subject: [PATCH 463/478] =?UTF-8?q?feat:=20GeoLocationService=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/GeoLocationService.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/spring/backend/core/application/GeoLocationService.java diff --git a/src/main/java/spring/backend/core/application/GeoLocationService.java b/src/main/java/spring/backend/core/application/GeoLocationService.java new file mode 100644 index 000000000..dfdff55d8 --- /dev/null +++ b/src/main/java/spring/backend/core/application/GeoLocationService.java @@ -0,0 +1,46 @@ +package spring.backend.core.application; + +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CityResponse; +import com.maxmind.geoip2.record.Location; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import spring.backend.core.util.geo.dto.response.Coordinate; + +import java.io.IOException; +import java.net.InetAddress; + +import static spring.backend.core.util.geo.GeoUtil.calculateDistanceBetweenTwoCoordinate; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class GeoLocationService { + private final DatabaseReader reader; + + public boolean checkUserLocation(String newIp, String savedIp) throws IOException, GeoIp2Exception { + Coordinate newIpCoordinate = getCoordinate(newIp); + Coordinate savedIpCoordinate = getCoordinate(savedIp); + + double distanceBetweenIp = calculateDistanceBetweenTwoCoordinate( + savedIpCoordinate.latitude(), + savedIpCoordinate.longitude(), + newIpCoordinate.latitude(), + newIpCoordinate.longitude() + ); + + return distanceBetweenIp > 100; + } + + private Coordinate getCoordinate(String ip) throws GeoIp2Exception, IOException { + InetAddress ipAddress = InetAddress.getByName(ip); + CityResponse response = reader.city(ipAddress); + Location location = response.getLocation(); + return Coordinate.of( + location.getLatitude(), + location.getLongitude() + ); + } +} From f82623255e071ec7fcc7cdcb66222778058e0b2a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:49:34 +0900 Subject: [PATCH 464/478] =?UTF-8?q?chore:=20gitignore=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index df033379a..6e2a09cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,3 @@ out/ ### Configuration ### src/main/resources/application-*.yml src/test/resources/application.yml -src/main/resources/maxmind From 0bbab74e3f20421572988810487c996014d10f4e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:53:17 +0900 Subject: [PATCH 465/478] =?UTF-8?q?feat:=20refreshToken=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20ip=EB=A5=BC=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/application/JwtService.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/spring/backend/core/application/JwtService.java b/src/main/java/spring/backend/core/application/JwtService.java index f01485510..8f1886a49 100644 --- a/src/main/java/spring/backend/core/application/JwtService.java +++ b/src/main/java/spring/backend/core/application/JwtService.java @@ -49,16 +49,18 @@ public String provideAccessToken(Member member) { member.getEmail(), member.getId(), Type.ACCESS, - ACCESS_EXPIRATION + ACCESS_EXPIRATION, + "" ); } - public String provideRefreshToken(Member member) { + public String provideRefreshToken(Member member, String ip) { return provideToken( member.getEmail(), member.getId(), Type.REFRESH, - REFRESH_EXPIRATION + REFRESH_EXPIRATION, + ip ); } @@ -97,18 +99,20 @@ public void validateTokenExpiration(String token) { } } - private String provideToken(String email, UUID id, Type type, long expiration) { + private String provideToken(String email, UUID id, Type type, long expiration, String ip) { Date expiryDate; Map claims; if (type == Type.ACCESS) { expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS)); claims = Map.of( "memberId", id.toString(), - "email", email, - "type", type.getType()); + "email", email + ); } else { expiryDate = Date.from(Instant.now().plus(expiration, ChronoUnit.DAYS)); - claims = Map.of(); + claims = Map.of( + "ip", ip + ); } return Jwts.builder() .claims(claims) From 97a0aa5abc7d85a60ed9e6eeb9d6b6cfc9de54aa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:54:14 +0900 Subject: [PATCH 466/478] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20ip=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EB=B0=9B?= =?UTF-8?q?=EC=95=84=EC=98=A8=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/HandleOAuthLoginService.java | 11 ++++++++--- .../auth/presentation/HandleOAuthLoginController.java | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java index f888d1196..917127b64 100644 --- a/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java +++ b/src/main/java/spring/backend/auth/application/HandleOAuthLoginService.java @@ -28,7 +28,7 @@ public class HandleOAuthLoginService { private final RefreshTokenService refreshTokenService; - public LoginResponse handleOAuthLogin(String providerName, String code, String state) { + public LoginResponse handleOAuthLogin(String providerName, String code, String state, String ip) { if (providerName == null || providerName.isEmpty()) { throw AuthenticationErrorCode.NOT_EXIST_PROVIDER.toException(); } @@ -36,11 +36,14 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s OAuthRestClient oAuthRestClient = oAuthRestClientFactory.getOAuthRestClient(provider); OAuthAccessTokenResponse oAuthAccessTokenResponse = oAuthRestClient.getAccessToken(code, state); + if (oAuthAccessTokenResponse == null) { log.error("[HandleOAuthLoginService] OAuth access token could not be retrieved."); throw AuthenticationErrorCode.ACCESS_TOKEN_NOT_ISSUED.toException(); } + OAuthResourceResponse oAuthResourceResponse = oAuthRestClient.getResource(oAuthAccessTokenResponse.getAccessToken()); + if (oAuthResourceResponse == null) { log.error("[HandleOAuthLoginService] OAuth resource could not be retrieved."); throw AuthenticationErrorCode.RESOURCE_SERVER_UNAVAILABLE.toException(); @@ -52,7 +55,9 @@ public LoginResponse handleOAuthLogin(String providerName, String code, String s .build(); Member member = createMemberWithOAuthService.createMemberWithOAuth(createMemberWithOAuthRequest); - refreshTokenService.saveRefreshToken(member); - return LoginResponse.of(jwtService.provideAccessToken(member), jwtService.provideRefreshToken(member), member); + String accessToken = jwtService.provideAccessToken(member); + String refreshToken = jwtService.provideRefreshToken(member, ip); + refreshTokenService.saveRefreshToken(refreshToken, member); + return LoginResponse.of(accessToken, refreshToken, member); } } diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index 93fb9c979..e774f9e8f 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -8,6 +8,7 @@ import spring.backend.auth.application.HandleOAuthLoginService; import spring.backend.auth.presentation.dto.response.LoginResponse; import spring.backend.auth.presentation.dto.response.LoginUserInfoResponse; +import spring.backend.core.configuration.argumentresolver.ClientIp; import spring.backend.core.presentation.RestResponse; @RestController @@ -19,8 +20,8 @@ public class HandleOAuthLoginController { @GetMapping("/{providerName}") public ResponseEntity> handleOAuthLogin(@RequestParam(value = "code", required = false) String code, - @RequestParam(value = "state", required = false) String state, @PathVariable String providerName) { - LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state); + @RequestParam(value = "state", required = false) String state, @PathVariable String providerName, @ClientIp String ip) { + LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state, ip); ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) .httpOnly(true) .path("/") From 7f084a58bc6eeecf111d794de0008b23523fe5d7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:55:28 +0900 Subject: [PATCH 467/478] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20RTR=EC=9D=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RotateAccessTokenController.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index 8ca02e7c9..dacd290c4 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -1,5 +1,6 @@ package spring.backend.auth.presentation; +import com.maxmind.geoip2.exception.GeoIp2Exception; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpHeaders; @@ -10,31 +11,41 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import spring.backend.auth.application.RotateAccessTokenService; -import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; -import spring.backend.auth.presentation.swagger.RotateAccessTokenSwagger; +import spring.backend.auth.presentation.dto.response.RotateTokenResponse; +import spring.backend.auth.presentation.swagger.RotateTokenSwagger; +import spring.backend.core.configuration.argumentresolver.ClientIp; import spring.backend.core.presentation.RestResponse; +import java.io.IOException; + import static org.springframework.http.ResponseCookie.from; @RestController @RequestMapping("/v1/token/rotate") @RequiredArgsConstructor @Log4j2 -public class RotateAccessTokenController implements RotateAccessTokenSwagger { +public class RotateAccessTokenController implements RotateTokenSwagger { private final RotateAccessTokenService rotateTokenService; @PostMapping - public ResponseEntity> rotateAccessToken( - @CookieValue(name = "refresh_token", required = false) String refreshToken - ) { - RotateAccessTokenResponse rotateAccessTokenResponse = rotateTokenService.rotateAccessToken(refreshToken); - ResponseCookie newAccessToken = from("access_token", rotateAccessTokenResponse.accessToken()) + public ResponseEntity> rotateToken( + @CookieValue(name = "refresh_token", required = false) String refreshToken, + @ClientIp String ip + ) throws IOException, GeoIp2Exception { + RotateTokenResponse rotateTokenResponse = rotateTokenService.rotateToken(refreshToken, ip); + ResponseCookie newAccessToken = from("access_token", rotateTokenResponse.accessToken()) + .httpOnly(true) + .path("/") + .build(); + + ResponseCookie newRefreshToken = from("refresh_token", rotateTokenResponse.refreshToken()) .httpOnly(true) .path("/") .build(); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, newAccessToken.toString()) + .header(HttpHeaders.SET_COOKIE, newRefreshToken.toString()) .build(); } } From 53cb10b6d605a37eea7f4c87baadd32efb2cf432 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:56:12 +0900 Subject: [PATCH 468/478] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=8B=9C=20100=20km=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=93=A4=EC=96=B4=EC=98=A8=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=EC=9D=84=20=EB=B0=98=EB=A0=A4=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/RotateAccessTokenService.java | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index db14e77a2..5717e3046 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -1,17 +1,18 @@ package spring.backend.auth.application; +import com.maxmind.geoip2.exception.GeoIp2Exception; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import spring.backend.auth.exception.AuthenticationErrorCode; import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; -import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; +import spring.backend.auth.presentation.dto.response.RotateTokenResponse; +import spring.backend.core.application.GeoLocationService; import spring.backend.core.application.JwtService; import spring.backend.member.domain.entity.Member; import spring.backend.member.domain.repository.MemberRepository; -import spring.backend.member.exception.MemberErrorCode; -import java.util.Optional; +import java.io.IOException; import java.util.UUID; @Service @@ -22,14 +23,30 @@ public class RotateAccessTokenService { private final JwtService jwtService; private final RefreshTokenService refreshTokenService; private final RefreshTokenRedisRepository refreshTokenRedisRepository; + private final GeoLocationService geoLocationService; - public RotateAccessTokenResponse rotateAccessToken(String refreshToken) { - if(refreshToken == null) { + public RotateTokenResponse rotateToken(String refreshToken, String newIp) throws IOException, GeoIp2Exception { + if (refreshToken == null) { throw AuthenticationErrorCode.MISSING_COOKIE_VALUE.toException(); } refreshTokenService.validateRefreshToken(refreshToken); + + String savedIp = jwtService.getPayload(refreshToken).get("ip", String.class); + + if (geoLocationService.checkUserLocation(newIp, savedIp)) { + refreshTokenService.deleteRefreshToken(refreshToken); + log.error("100km 밖에서 토큰 재발급을 시도했습니다."); + throw AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_100KM.toException(); + } + UUID memberId = UUID.fromString(refreshTokenRedisRepository.findByRefreshToken(refreshToken)); Member member = memberRepository.findById(memberId); - return new RotateAccessTokenResponse(jwtService.provideAccessToken(Optional.ofNullable(member).orElseThrow(MemberErrorCode.NOT_EXIST_MEMBER::toException))); + String newAccessToken = jwtService.provideAccessToken(member); + String newRefreshToken = jwtService.provideRefreshToken(member, newIp); + refreshTokenService.saveRefreshToken(newRefreshToken, member); + return new RotateTokenResponse( + newAccessToken, + newRefreshToken + ); } } From d4932af79d85e466ca9b4d445658b80c9a3e4455 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:56:48 +0900 Subject: [PATCH 469/478] =?UTF-8?q?feat:=20Redis=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/application/RefreshTokenService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RefreshTokenService.java b/src/main/java/spring/backend/auth/application/RefreshTokenService.java index 3929cc739..b80d3fe1f 100644 --- a/src/main/java/spring/backend/auth/application/RefreshTokenService.java +++ b/src/main/java/spring/backend/auth/application/RefreshTokenService.java @@ -24,10 +24,8 @@ public RefreshTokenService(JwtService jwtService, @Value("${jwt.refresh-token-ex this.refreshTokenRepository = refreshTokenRepository; } - public String saveRefreshToken(Member member) { - String refreshToken = jwtService.provideRefreshToken(member); + public void saveRefreshToken(String refreshToken, Member member) { refreshTokenRepository.save(refreshToken, member.getId(), REFRESH_TOKEN_EXPIRATION, convertChronoUnitToTimeUnit(ChronoUnit.DAYS)); - return refreshToken; } public void validateRefreshToken(String refreshToken) { From aecca6652a50e87cadb2bd7c72cf8172e60a419d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:57:15 +0900 Subject: [PATCH 470/478] =?UTF-8?q?feat:=20100km=20=EC=9D=B4=EC=83=81=20?= =?UTF-8?q?=EB=96=A8=EC=96=B4=EC=A7=84=20=EA=B3=B3=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20=EC=9A=94=EC=B2=AD=20=EA=B1=B0=EC=A0=88=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EB=A7=8C=EB=93=A0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/backend/auth/exception/AuthenticationErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index 02a8febcc..ffd3ee02f 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -29,6 +29,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { INVALID_MEMBER_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입을 위한 사용자 조건이 유효하지 않습니다."), NOT_EXIST_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입 요청이 유효하지 않습니다."), INVALID_BIRTH_YEAR(HttpStatus.BAD_REQUEST, "출생년도는 현재 연도와 100년 전 사이여야 합니다."), + TOKEN_ROTATE_ATTEMPT_FROM_100KM(HttpStatus.UNAUTHORIZED, "100km 이상 떨어진 위치에서 토큰 재발급을 시도했습니다."), FAILED_TO_EXTRACT_MEMBER_ID_FROM_EXPIRED_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "만료된 액세스 토큰에서 회원 ID를 추출하는데 실패했습니다."); private final HttpStatus httpStatus; From 28260cc198bf59ae9700ff1df85abdc59565ab76 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:57:40 +0900 Subject: [PATCH 471/478] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20dto=20=EB=82=B4=EC=9A=A9=EC=9D=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dto/response/RotateAccessTokenResponse.java | 4 ---- .../auth/presentation/dto/response/RotateTokenResponse.java | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java create mode 100644 src/main/java/spring/backend/auth/presentation/dto/response/RotateTokenResponse.java diff --git a/src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java deleted file mode 100644 index 0cd8755a7..000000000 --- a/src/main/java/spring/backend/auth/presentation/dto/response/RotateAccessTokenResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package spring.backend.auth.presentation.dto.response; - -public record RotateAccessTokenResponse(String accessToken) { -} diff --git a/src/main/java/spring/backend/auth/presentation/dto/response/RotateTokenResponse.java b/src/main/java/spring/backend/auth/presentation/dto/response/RotateTokenResponse.java new file mode 100644 index 000000000..b1902eae0 --- /dev/null +++ b/src/main/java/spring/backend/auth/presentation/dto/response/RotateTokenResponse.java @@ -0,0 +1,4 @@ +package spring.backend.auth.presentation.dto.response; + +public record RotateTokenResponse(String accessToken, String refreshToken) { +} From bca23de103f596c4c8149bd5d23485cee0169cef Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:59:05 +0900 Subject: [PATCH 472/478] =?UTF-8?q?feat:=20RotateTokenSwagger=20=EB=B0=8F?= =?UTF-8?q?=20Redis=20=ED=82=A4-=EB=B0=B8=EB=A5=98=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RefreshTokenRepository.java | 1 + .../RefreshTokenRedisRepository.java | 13 +++++++++++++ ...kenSwagger.java => RotateTokenSwagger.java} | 18 +++++++++++------- 3 files changed, 25 insertions(+), 7 deletions(-) rename src/main/java/spring/backend/auth/presentation/swagger/{RotateAccessTokenSwagger.java => RotateTokenSwagger.java} (71%) diff --git a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java index 0e6b92b7b..b34abf16b 100644 --- a/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java +++ b/src/main/java/spring/backend/auth/domain/repository/RefreshTokenRepository.java @@ -7,4 +7,5 @@ public interface RefreshTokenRepository { void save(String refreshToken, UUID memberId, Long expireTime, TimeUnit timeUnit); String findByRefreshToken(String refreshToken); void deleteByRefreshToken(String refreshToken); + void deleteAll(); } diff --git a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java index b4f93bd98..0beed659c 100644 --- a/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java +++ b/src/main/java/spring/backend/auth/infrastructure/redis/repository/RefreshTokenRedisRepository.java @@ -55,4 +55,17 @@ public void deleteByRefreshToken(String refreshToken) { throw GlobalErrorCode.INTERNAL_ERROR.toException(); } } + + @Override + public void deleteAll() { + try { + redisTemplate.getConnectionFactory().getConnection().flushDb(); + } catch (RedisConnectionException e) { + log.error("Redis 연결 오류 : {}", e.getMessage()); + throw GlobalErrorCode.REDIS_CONNECTION_ERROR.toException(); + } catch (Exception e) { + throw GlobalErrorCode.INTERNAL_ERROR.toException(); + } + } + } diff --git a/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java b/src/main/java/spring/backend/auth/presentation/swagger/RotateTokenSwagger.java similarity index 71% rename from src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java rename to src/main/java/spring/backend/auth/presentation/swagger/RotateTokenSwagger.java index 6ab8c399f..406be4c1e 100644 --- a/src/main/java/spring/backend/auth/presentation/swagger/RotateAccessTokenSwagger.java +++ b/src/main/java/spring/backend/auth/presentation/swagger/RotateTokenSwagger.java @@ -1,25 +1,29 @@ package spring.backend.auth.presentation.swagger; +import com.maxmind.geoip2.exception.GeoIp2Exception; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import spring.backend.auth.presentation.dto.response.RotateAccessTokenResponse; -import spring.backend.core.configuration.swagger.ApiErrorCode; -import spring.backend.core.presentation.RestResponse; import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.presentation.dto.response.RotateTokenResponse; +import spring.backend.core.configuration.swagger.ApiErrorCode; import spring.backend.core.exception.error.GlobalErrorCode; +import spring.backend.core.presentation.RestResponse; + +import java.io.IOException; @Tag(name = "Auth", description = "인증/인가") -public interface RotateAccessTokenSwagger { +public interface RotateTokenSwagger { @Operation( summary = "토큰 재발급 API", description = "Access Token이 만료된 경우, Refresh Token을 이용하여 새로운 Access Token을 발급합니다", operationId = "/v1/token/rotate" ) @ApiErrorCode({GlobalErrorCode.class, AuthenticationErrorCode.class}) - ResponseEntity> rotateAccessToken( + ResponseEntity> rotateToken( @Parameter(description = "쿠키에 있는 refresh_token", required = false) - String refreshToken - ); + String refreshToken, + @Parameter(hidden = true) String ip + ) throws IOException, GeoIp2Exception; } From 6eb35dcab33249f14d7e51a2c6421bed7f75cd0b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 15:59:13 +0900 Subject: [PATCH 473/478] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/JwtServiceTest.java | 2 +- .../application/RefreshTokenServiceTest.java | 6 +- .../RotateAccessTokenServiceTest.java | 71 ++++++++++++++++++- .../spring/backend/core/util/GeoUtilTest.java | 49 +++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/test/java/spring/backend/core/util/GeoUtilTest.java diff --git a/src/test/java/spring/backend/core/application/JwtServiceTest.java b/src/test/java/spring/backend/core/application/JwtServiceTest.java index 060fc2b00..4e12021e3 100644 --- a/src/test/java/spring/backend/core/application/JwtServiceTest.java +++ b/src/test/java/spring/backend/core/application/JwtServiceTest.java @@ -74,4 +74,4 @@ void validateTokenExpirationWithExpiredJwt() { DomainException ex = assertThrows(DomainException.class, () -> jwtService.validateTokenExpiration(expiredJwt), "만료된 토큰입니다."); assertThat(ex.getCode()).isEqualTo(AuthenticationErrorCode.EXPIRED_TOKEN.name()); } -} \ No newline at end of file +} diff --git a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java index c08f12243..b9a90979c 100644 --- a/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RefreshTokenServiceTest.java @@ -26,6 +26,9 @@ public class RefreshTokenServiceTest { @Autowired private RefreshTokenRedisRepository refreshTokenRedisRepository; + @Autowired + private JwtService jwtService; + private final UUID memberId = UUID.randomUUID(); private final Member member = Member.builder() @@ -56,7 +59,8 @@ static void afterAll(@Qualifier("redisConnectionFactory") LettuceConnectionFacto @Test void saveRefreshTokenWhenTokenReleased() { // when - String refreshToken = refreshTokenService.saveRefreshToken(member); + String refreshToken = jwtService.provideRefreshToken(member, ""); + refreshTokenService.saveRefreshToken(refreshToken, member); // then assertThat(member.getId().toString()).isEqualTo(refreshTokenRedisRepository.findByRefreshToken(refreshToken)); } diff --git a/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java index 114167f96..9d3cb61f9 100644 --- a/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java @@ -1,25 +1,94 @@ package spring.backend.core.application; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import spring.backend.auth.application.RefreshTokenService; import spring.backend.auth.application.RotateAccessTokenService; +import spring.backend.auth.exception.AuthenticationErrorCode; +import spring.backend.auth.infrastructure.redis.repository.RefreshTokenRedisRepository; import spring.backend.core.exception.DomainException; +import spring.backend.member.domain.entity.Member; +import spring.backend.member.infrastructure.persistence.jpa.adapter.MemberRepositoryImpl; +import java.io.IOException; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertThrows; @SpringBootTest public class RotateAccessTokenServiceTest { @Autowired private RotateAccessTokenService rotateAccessTokenService; + @Autowired + private JwtService jwtService; + + @Autowired + private GeoLocationService geoLocationService; + + @Autowired + private RefreshTokenRedisRepository refreshTokenRedisRepository; + + @Autowired + private RefreshTokenService refreshTokenService; + + private String newIp; + private String savedIp; + + + private final UUID memberId = UUID.randomUUID(); + + private final Member member = Member.builder() + .id(memberId) + .email("test@test.com") + .build(); + @Autowired + private MemberRepositoryImpl memberRepositoryImpl; + + @BeforeEach + void setUp() { + newIp = "210.180.165.70"; // 부산 + savedIp = "110.12.115.169"; // 서울 + } + + @AfterEach + void tearDown() { + refreshTokenRedisRepository.deleteAll(); + } + @DisplayName("Cookie에 refreshToken이 존재하지 않는 경우 예외를 발생시킨다.") @Test void throwExceptionWhenRefreshTokenNotExistsInCookie() { // when, then - assertThatThrownBy(() -> rotateAccessTokenService.rotateAccessToken(null)) + assertThatThrownBy(() -> rotateAccessTokenService.rotateToken(null, "")) .isInstanceOf(DomainException.class) .hasMessage("쿠키값이 존재하지 않습니다."); } + + @DisplayName("100km 밖에서 토큰 재발급을 시도한 경우 예외를 발생시킨다.") + @Test + void throwExceptionWhenTokenRotateAttemptFrom100km() throws IOException, GeoIp2Exception { + boolean isOver100km = geoLocationService.checkUserLocation(newIp, savedIp); + // when, then + assertThat(isOver100km).isTrue(); + } + + @DisplayName("Redis에 저장된 RefreshToken의 IP와 새로운 IP가 100km 이상 차이가 나는 경우 예외를 발생시킨다.") + @Test + void throwExceptionWhenIpDistanceIsOver100km() throws Exception { + // given + memberRepositoryImpl.save(member); + String refreshToken = jwtService.provideRefreshToken(member, savedIp); + refreshTokenService.saveRefreshToken(refreshToken, member); + // when, then + DomainException ex = assertThrows(DomainException.class, () -> rotateAccessTokenService.rotateToken(refreshToken, newIp), "100km 밖에서 토큰 재발급을 시도했습니다."); + assertThat(ex.getCode()).isEqualTo(AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_100KM.name()); + } } diff --git a/src/test/java/spring/backend/core/util/GeoUtilTest.java b/src/test/java/spring/backend/core/util/GeoUtilTest.java new file mode 100644 index 000000000..459d317a2 --- /dev/null +++ b/src/test/java/spring/backend/core/util/GeoUtilTest.java @@ -0,0 +1,49 @@ +package spring.backend.core.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import spring.backend.core.util.geo.GeoUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GeoUtilTest { + @Test + @DisplayName("서울(37.5665, 126.9780) - 부산(35.1796, 129.0756) 거리 검증") + void seoulToBusan() { + // Given + double seoulLat = 37.5665; + double seoulLon = 126.9780; + double busanLat = 35.1796; + double busanLon = 129.0756; + + // When + double distance = GeoUtil.calculateDistanceBetweenTwoCoordinate( + seoulLat, seoulLon, + busanLat, busanLon + ); + + // Then (실제 거리: 약 325km) + assertEquals(325.0, distance, 10.0); + } + + @Test + @DisplayName("서울(37.5665, 126.9780) - 인천(37.4500, 126.7000) 거리 검증") + void seoulToIncheon() { + double distance = GeoUtil.calculateDistanceBetweenTwoCoordinate( + 37.5665, 126.9780, + 37.4500, 126.7000 + ); + assertEquals(28.0, distance, 2.0); + } + + @Test + @DisplayName("동일 좌표 거리 계산") + void sameCoordinates() { + double distance = GeoUtil.calculateDistanceBetweenTwoCoordinate( + 37.5665, 126.9780, + 37.5665, 126.9780 + ); + assertEquals(0.0, distance, 0.0); + } + +} From 08afb01e66c57abb77f1aed51aa53b7bad003d49 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 20 Feb 2025 16:00:42 +0900 Subject: [PATCH 474/478] =?UTF-8?q?chore:=20maxmind=20db=EB=A5=BC=20.gitig?= =?UTF-8?q?nore=EC=97=90=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6e2a09cdf..e08eee973 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ out/ ### Configuration ### src/main/resources/application-*.yml +src/main/resources/maxmind src/test/resources/application.yml From d13dd1d49136c447de405d5928acb9d9f375c309 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 24 Feb 2025 18:54:19 +0900 Subject: [PATCH 475/478] =?UTF-8?q?chore:=20ci/cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 34ede148d..8ff846f21 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,6 +28,18 @@ jobs: with: ssh-private-key: ${{ secrets.NCP_SSH_PRIVATE_KEY }} + - name: Download GeoLite2-City Database + run: | + mkdir -p src/main/resources/maxmind + + curl -L -u ${{ secrets.GEOIP_ACCOUNT_ID }}:${{ secrets.GEOIP_LICENSE }} \ + 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=${{ secrets.GEOIP_LICENSE }}' \ + -o GeoLite2-City.tar.gz + + tar -xzf GeoLite2-City.tar.gz --wildcards --strip-components 1 -C src/main/resources/maxmind '*.mmdb' + + rm GeoLite2-City.tar.gz + - name: Gradle 캐시 적용 uses: actions/cache@v3 with: From c9bd777d6e14e7f1f511882c9352c7416abc0b5d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 24 Feb 2025 23:32:49 +0900 Subject: [PATCH 476/478] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/application/RotateAccessTokenService.java | 4 ++-- .../backend/auth/exception/AuthenticationErrorCode.java | 2 +- .../core/application/RotateAccessTokenServiceTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java index 5717e3046..d2905304b 100644 --- a/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java +++ b/src/main/java/spring/backend/auth/application/RotateAccessTokenService.java @@ -35,8 +35,8 @@ public RotateTokenResponse rotateToken(String refreshToken, String newIp) throws if (geoLocationService.checkUserLocation(newIp, savedIp)) { refreshTokenService.deleteRefreshToken(refreshToken); - log.error("100km 밖에서 토큰 재발급을 시도했습니다."); - throw AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_100KM.toException(); + log.error("유효하지 않은 위치에서 토큰 재발급을 시도했습니다."); + throw AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_INVALID_LOCATION.toException(); } UUID memberId = UUID.fromString(refreshTokenRedisRepository.findByRefreshToken(refreshToken)); diff --git a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java index ffd3ee02f..a422fac27 100644 --- a/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java +++ b/src/main/java/spring/backend/auth/exception/AuthenticationErrorCode.java @@ -29,7 +29,7 @@ public enum AuthenticationErrorCode implements BaseErrorCode { INVALID_MEMBER_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입을 위한 사용자 조건이 유효하지 않습니다."), NOT_EXIST_SIGN_UP_CONDITION(HttpStatus.BAD_REQUEST, "회원가입 요청이 유효하지 않습니다."), INVALID_BIRTH_YEAR(HttpStatus.BAD_REQUEST, "출생년도는 현재 연도와 100년 전 사이여야 합니다."), - TOKEN_ROTATE_ATTEMPT_FROM_100KM(HttpStatus.UNAUTHORIZED, "100km 이상 떨어진 위치에서 토큰 재발급을 시도했습니다."), + TOKEN_ROTATE_ATTEMPT_FROM_INVALID_LOCATION(HttpStatus.UNAUTHORIZED, "유효하지 않은 위치에서 토큰 재발급을 시도했습니다."), FAILED_TO_EXTRACT_MEMBER_ID_FROM_EXPIRED_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "만료된 액세스 토큰에서 회원 ID를 추출하는데 실패했습니다."); private final HttpStatus httpStatus; diff --git a/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java index 9d3cb61f9..7477e8303 100644 --- a/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java +++ b/src/test/java/spring/backend/core/application/RotateAccessTokenServiceTest.java @@ -89,6 +89,6 @@ void throwExceptionWhenIpDistanceIsOver100km() throws Exception { refreshTokenService.saveRefreshToken(refreshToken, member); // when, then DomainException ex = assertThrows(DomainException.class, () -> rotateAccessTokenService.rotateToken(refreshToken, newIp), "100km 밖에서 토큰 재발급을 시도했습니다."); - assertThat(ex.getCode()).isEqualTo(AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_100KM.name()); + assertThat(ex.getCode()).isEqualTo(AuthenticationErrorCode.TOKEN_ROTATE_ATTEMPT_FROM_INVALID_LOCATION.name()); } } From 249c9eec7a988b01d697614eb49e43f7419b082f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 24 Feb 2025 23:34:23 +0900 Subject: [PATCH 477/478] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=9C=A0=ED=9A=A8=20=EA=B1=B0=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/application/GeoLocationService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/spring/backend/core/application/GeoLocationService.java b/src/main/java/spring/backend/core/application/GeoLocationService.java index dfdff55d8..72595637e 100644 --- a/src/main/java/spring/backend/core/application/GeoLocationService.java +++ b/src/main/java/spring/backend/core/application/GeoLocationService.java @@ -4,8 +4,8 @@ import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.record.Location; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import spring.backend.core.util.geo.dto.response.Coordinate; @@ -15,10 +15,15 @@ import static spring.backend.core.util.geo.GeoUtil.calculateDistanceBetweenTwoCoordinate; @Service -@RequiredArgsConstructor @Log4j2 public class GeoLocationService { private final DatabaseReader reader; + private final int MAX_DISTANCE; + + public GeoLocationService(@Value("${geo.max-distance}") int maxDistance, DatabaseReader reader) { + this.reader = reader; + this.MAX_DISTANCE = maxDistance; + } public boolean checkUserLocation(String newIp, String savedIp) throws IOException, GeoIp2Exception { Coordinate newIpCoordinate = getCoordinate(newIp); @@ -31,7 +36,7 @@ public boolean checkUserLocation(String newIp, String savedIp) throws IOExceptio newIpCoordinate.longitude() ); - return distanceBetweenIp > 100; + return distanceBetweenIp > MAX_DISTANCE; } private Coordinate getCoordinate(String ip) throws GeoIp2Exception, IOException { From a3b858555fb7e16cbc9bd51df3c51552c3ba2d05 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 27 Feb 2025 11:25:12 +0900 Subject: [PATCH 478/478] =?UTF-8?q?feat:=20=EC=BF=A0=ED=82=A4=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EC=8B=9C=20secure,=20sameSite=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/presentation/HandleOAuthLoginController.java | 4 ++++ .../auth/presentation/RotateAccessTokenController.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java index e774f9e8f..0b9c3ffbd 100644 --- a/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java +++ b/src/main/java/spring/backend/auth/presentation/HandleOAuthLoginController.java @@ -24,10 +24,14 @@ public ResponseEntity> handleOAuthLogin(@Req LoginResponse loginResponse = handleOAuthLoginService.handleOAuthLogin(providerName, code, state, ip); ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", loginResponse.accessToken()) .httpOnly(true) + .secure(true) + .sameSite("None") .path("/") .build(); ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", loginResponse.refreshToken()) .httpOnly(true) + .secure(true) + .sameSite("None") .path("/") .build(); diff --git a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java index dacd290c4..28a7202a4 100644 --- a/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java +++ b/src/main/java/spring/backend/auth/presentation/RotateAccessTokenController.java @@ -35,11 +35,15 @@ public ResponseEntity> rotateToken( RotateTokenResponse rotateTokenResponse = rotateTokenService.rotateToken(refreshToken, ip); ResponseCookie newAccessToken = from("access_token", rotateTokenResponse.accessToken()) .httpOnly(true) + .secure(true) + .sameSite("None") .path("/") .build(); ResponseCookie newRefreshToken = from("refresh_token", rotateTokenResponse.refreshToken()) .httpOnly(true) + .secure(true) + .sameSite("None") .path("/") .build();