From 95ff712d31df2f07cb770880d32d0e0b7cac225f Mon Sep 17 00:00:00 2001 From: queenriwon Date: Mon, 24 Mar 2025 22:48:55 +0900 Subject: [PATCH 001/164] =?UTF-8?q?feat(global):=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 내용 - ErrorResponse: HttpStatus를 입력받아 status, code 부분 작성할 수 있도록 수정 - GlobalExceptionHandler: 여러개 커스텀예외를 처리할 수 있도록 부모 예외 클래스를 다룸 ### 추가한 내용 - CustomException과 각 에러 코드에 관한 예외클래스(CustomException을 상속) - CustomException에는 HttpStatus와 메세지 정보를 가지고 있음 - ErrorCode Enum 자료형을 이용하여 에러 코드와 메세지를 관리할 수 있도록 정의 --- .../global/entity/ErrorResponse.java | 11 +++++--- .../global/exception/BadRequestException.java | 12 +++++++++ .../global/exception/CustomException.java | 25 +++++++++++++++++++ .../eightyage/global/exception/ErrorCode.java | 20 +++++++++++++++ .../global/exception/ForbiddenException.java | 12 +++++++++ .../exception/GlobalExceptionHandler.java | 12 ++++----- .../global/exception/NotFoundException.java | 12 +++++++++ .../exception/UnauthorizedException.java | 11 ++++++++ 8 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/exception/BadRequestException.java create mode 100644 src/main/java/com/example/eightyage/global/exception/CustomException.java create mode 100644 src/main/java/com/example/eightyage/global/exception/ErrorCode.java create mode 100644 src/main/java/com/example/eightyage/global/exception/ForbiddenException.java create mode 100644 src/main/java/com/example/eightyage/global/exception/NotFoundException.java create mode 100644 src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java diff --git a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java b/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java index e442661..42e7332 100644 --- a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java +++ b/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java @@ -3,6 +3,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.http.HttpStatus; @Getter @Setter @@ -15,9 +16,13 @@ public class ErrorResponse { private String message; - public ErrorResponse(String statusName, Integer code, String message) { - this.status = statusName; - this.code = code; + public ErrorResponse(HttpStatus httpStatus, String message) { + this.status = httpStatus.name(); + this.code = httpStatus.value(); this.message = message; } + + public static ErrorResponse of(HttpStatus httpStatus, String message) { + return new ErrorResponse(httpStatus, message); + } } diff --git a/src/main/java/com/example/eightyage/global/exception/BadRequestException.java b/src/main/java/com/example/eightyage/global/exception/BadRequestException.java new file mode 100644 index 0000000..e6ae476 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/BadRequestException.java @@ -0,0 +1,12 @@ +package com.example.eightyage.global.exception; + +public class BadRequestException extends CustomException { + + public BadRequestException() { + super(ErrorCode.BAD_REQUEST); + } + + public BadRequestException(String message) { + super(ErrorCode.BAD_REQUEST, message); + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/CustomException.java b/src/main/java/com/example/eightyage/global/exception/CustomException.java new file mode 100644 index 0000000..1bc3086 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/CustomException.java @@ -0,0 +1,25 @@ +package com.example.eightyage.global.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class CustomException extends RuntimeException { + + private final HttpStatus httpStatus; + private final String message; + + // 예외 던질시 기본 메세지 출력 + public CustomException(ErrorCode errorCode) { + super(errorCode.getDefaultMessage()); + this.httpStatus = errorCode.getStatus(); + this.message = errorCode.getDefaultMessage(); + } + + // 예외 던질시 메세지 출력 + public CustomException(ErrorCode errorCode, String message) { + super(message); + this.httpStatus = errorCode.getStatus(); + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorCode.java b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java new file mode 100644 index 0000000..8063695 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java @@ -0,0 +1,20 @@ +package com.example.eightyage.global.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + AUTHORIZATION(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "찾지 못했습니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다."); + + private final HttpStatus status; + private final String defaultMessage; + + ErrorCode(HttpStatus status, String defaultMessage) { + this.status = status; + this.defaultMessage = defaultMessage; + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java b/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java new file mode 100644 index 0000000..3cd553e --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java @@ -0,0 +1,12 @@ +package com.example.eightyage.global.exception; + +public class ForbiddenException extends CustomException { + + public ForbiddenException() { + super(ErrorCode.FORBIDDEN); + } + + public ForbiddenException(String message) { + super(ErrorCode.FORBIDDEN,message); + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 2a7de39..1ca08b0 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -1,18 +1,16 @@ package com.example.eightyage.global.exception; +import com.example.eightyage.global.entity.ErrorResponse; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.Map; - @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(InvalidRequestException.class) - public ResponseEntity> invalidRequestExceptionException(InvalidRequestException ex) { - HttpStatus status = HttpStatus.BAD_REQUEST; - return getErrorResponse(status, ex.getMessage()); + @ExceptionHandler(CustomException.class) + public ErrorResponse invalidRequestExceptionException(CustomException ex) { + HttpStatus httpStatus = ex.getHttpStatus(); + return ErrorResponse.of(httpStatus, ex.getMessage()); } } diff --git a/src/main/java/com/example/eightyage/global/exception/NotFoundException.java b/src/main/java/com/example/eightyage/global/exception/NotFoundException.java new file mode 100644 index 0000000..9233a87 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/NotFoundException.java @@ -0,0 +1,12 @@ +package com.example.eightyage.global.exception; + +public class NotFoundException extends CustomException { + public NotFoundException() { + super(ErrorCode.NOT_FOUND); + } + + public NotFoundException(String message) { + super(ErrorCode.NOT_FOUND, message); + } +} + diff --git a/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java b/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java new file mode 100644 index 0000000..a347954 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java @@ -0,0 +1,11 @@ +package com.example.eightyage.global.exception; + +public class UnauthorizedException extends CustomException { + + public UnauthorizedException() { + super(ErrorCode.AUTHORIZATION); + } + public UnauthorizedException(String message) { + super(ErrorCode.AUTHORIZATION, message); + } +} From 33e316fdc7fe323fae6adf225ae6a39b3877fbf7 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:00:49 +0900 Subject: [PATCH 002/164] Create bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4b3c320 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,35 @@ +name: "🐞 Bug Report" +description: "버그 신고용 템플릿입니다." +labels: ["bug"] +body: + - type: textarea + attributes: + label: 📄 버그 설명 + description: 어떤 문제가 발생했는지 자세히 적어주세요. + placeholder: 예시) 로그인 버튼을 눌렀는데 반응이 없습니다. + validations: + required: true + + - type: textarea + attributes: + label: 🪛 재현 방법 + description: 버그를 어떻게 재현할 수 있는지 단계별로 알려주세요. + placeholder: | + 1. 페이지 접속 + 2. 로그인 버튼 클릭 + 3. 아무 반응 없음 + validations: + required: true + + - type: textarea + attributes: + label: ✅ 기대 동작 + description: 원래 어떻게 동작했어야 하는지 작성해주세요. + placeholder: 로그인 성공 후 대시보드로 이동해야 합니다. + validations: + required: true + + - type: textarea + attributes: + label: 📎 참고 자료 (스크린샷 등) + description: 필요하다면 이미지, 로그 등 추가해주세요. From 61777f465fbdb5ed603fef248e957eeb2bbfaf46 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:07:09 +0900 Subject: [PATCH 003/164] Update feature_request.yml --- .github/ISSUE_TEMPLATE/feature_request.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 401d92d..030dacb 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,12 +1,22 @@ name: "✨ Feature" -description: "새로운 기능 추가" +description: "새로운 기능 추가용 템플릿입니다." labels: ["feature"] body: + - type: markdown + attributes: + value: | + 📝 **제목 작성 예시** + ``` + [FEATURE] 사용자 로그인 기능 추가 + [BUG] 로그인 실패 시 에러 발생 + ``` + 위 형식 참고해서 제목을 작성해 주세요! + - type: textarea attributes: label: 📄 설명 description: 새로운 기능에 대한 설명을 작성해 주세요. - placeholder: 자세히 적을수록 좋습니다! + placeholder: 자세히 적을수록 좋습니다. validations: required: true @@ -14,7 +24,7 @@ body: attributes: label: ✅ 작업할 내용 description: 할 일을 체크박스 형태로 작성해주세요. - placeholder: 최대한 세분화 해서 적어주세요! + placeholder: 예상 작업 내용을 세분화 해서 적어주세요. validations: required: true @@ -22,3 +32,4 @@ body: attributes: label: 🙋🏻 참고 자료 description: 참고 자료가 있다면 작성해 주세요. + From 1da623b6f51db008d6ada71042634bc756683924 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:16:53 +0900 Subject: [PATCH 004/164] Update feature_request.yml --- .github/ISSUE_TEMPLATE/feature_request.yml | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 030dacb..e99da5d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,30 +1,30 @@ name: "✨ Feature" description: "새로운 기능 추가용 템플릿입니다." labels: ["feature"] -body: - - type: markdown - attributes: - value: | - 📝 **제목 작성 예시** - ``` - [FEATURE] 사용자 로그인 기능 추가 - [BUG] 로그인 실패 시 에러 발생 - ``` - 위 형식 참고해서 제목을 작성해 주세요! - - type: textarea attributes: label: 📄 설명 - description: 새로운 기능에 대한 설명을 작성해 주세요. + description: | + 📝 제목 작성 예시: + ``` + [FEATURE] 사용자 로그인 기능 추가 + [BUG] 로그인 실패 시 에러 발생 + ``` + 제목은 위 형식을 참고해서 작성해 주세요! placeholder: 자세히 적을수록 좋습니다. validations: required: true - - type: textarea - attributes: - label: ✅ 작업할 내용 - description: 할 일을 체크박스 형태로 작성해주세요. - placeholder: 예상 작업 내용을 세분화 해서 적어주세요. +- type: textarea + attributes: + label: ✅ 작업할 내용 + description: 할 일을 체크박스 형태로 작성해주세요. + value: | + - [ ] 작업 내용 + - [ ] 작업 내용 + - [ ] 작업 내용 + - [ ] 작업 내용 + render: markdown validations: required: true @@ -32,4 +32,3 @@ body: attributes: label: 🙋🏻 참고 자료 description: 참고 자료가 있다면 작성해 주세요. - From b59e07b2d9c2196af52cb5e7c0a7d01caf52affd Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:18:13 +0900 Subject: [PATCH 005/164] Update feature_request.yml --- .github/ISSUE_TEMPLATE/feature_request.yml | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index e99da5d..d39de6f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,30 +1,30 @@ name: "✨ Feature" description: "새로운 기능 추가용 템플릿입니다." labels: ["feature"] +body: - type: textarea attributes: label: 📄 설명 - description: | - 📝 제목 작성 예시: - ``` - [FEATURE] 사용자 로그인 기능 추가 - [BUG] 로그인 실패 시 에러 발생 - ``` - 제목은 위 형식을 참고해서 작성해 주세요! + description: 새로운 기능에 대한 설명을 작성해 주세요. 아래는 제목 작성 예시입니다. + ``` + [FEATURE] 사용자 로그인 기능 추가 + [BUG] 로그인 실패 시 에러 발생 + ``` + 제목은 위 형식 참고해서 작성해 주세요! placeholder: 자세히 적을수록 좋습니다. validations: required: true -- type: textarea - attributes: - label: ✅ 작업할 내용 - description: 할 일을 체크박스 형태로 작성해주세요. - value: | - - [ ] 작업 내용 - - [ ] 작업 내용 - - [ ] 작업 내용 - - [ ] 작업 내용 - render: markdown + - type: textarea + attributes: + label: ✅ 작업할 내용 + description: 할 일을 체크박스 형태로 작성해주세요. + value: | + - [ ] 작업 내용 + - [ ] 작업 내용 + - [ ] 작업 내용 + - [ ] 작업 내용 + render: markdown validations: required: true From e85d256020d95a569549ae7ce3095777c4fa2352 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Mon, 24 Mar 2025 23:44:30 +0900 Subject: [PATCH 006/164] =?UTF-8?q?feat(auth):=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - User 엔티티에 email, nickname, password, userRole 정보를 저장 - UserRole에서 유저의 권한은 유저(USER_ROLE), 관리자(ADMIN_ROLE)로 정의 ### 수정한 내용 - build.gradle - aws S3 의존성을 추가하고 아무 설정을 해주지 않으면 에러발생 -> 주석처리 - 추후 S3를 사용할 때 주석해제하여 사용할 것 - spring security에 관한 의존성은 바로 확인 가능하도록 수정 --- build.gradle | 6 ++-- .../eightyage/domain/auth/controller/.gitkeep | 0 .../eightyage/domain/auth/entity/.gitkeep | 0 .../eightyage/domain/auth/entity/User.java | 27 +++++++++++++++++ .../domain/auth/entity/UserRole.java | 29 +++++++++++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/auth/controller/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/auth/entity/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/auth/entity/User.java create mode 100644 src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java diff --git a/build.gradle b/build.gradle index 55192ab..2bc6c71 100644 --- a/build.gradle +++ b/build.gradle @@ -32,12 +32,14 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // spring security testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // spring cloud AWS S3 - implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' +// implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/User.java b/src/main/java/com/example/eightyage/domain/auth/entity/User.java new file mode 100644 index 0000000..27c1cf5 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/entity/User.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.auth.entity; + +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor +public class User extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String email; + + private String nickname; + + private String password; + + @Enumerated(EnumType.STRING) + private UserRole userRole; + +} diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java b/src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java new file mode 100644 index 0000000..1f23602 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.auth.entity; + +import com.example.eightyage.global.exception.UnauthorizedException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +@Getter +@RequiredArgsConstructor +public enum UserRole { + + ROLE_USER(Authority.USER), + ROLE_ADMIN(Authority.ADMIN); + + private final String userRole; + + public static UserRole of(String role) { + return Arrays.stream(UserRole.values()) + .filter(r -> r.getUserRole().equalsIgnoreCase(role)) + .findFirst() + .orElseThrow(() -> new UnauthorizedException("유효하지 않은 UserRole")); + } + + public static class Authority { + public static final String USER = "ROLE_USER"; + public static final String ADMIN = "ROLE_ADMIN"; + } +} From aeffb690ca264c0c995aa481f8da7af9cecb9cb8 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:57:17 +0900 Subject: [PATCH 007/164] Update issue templates --- .../feature-request-issue-template.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature-request-issue-template.md diff --git a/.github/ISSUE_TEMPLATE/feature-request-issue-template.md b/.github/ISSUE_TEMPLATE/feature-request-issue-template.md new file mode 100644 index 0000000..13806a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request-issue-template.md @@ -0,0 +1,32 @@ +--- +name: Feature Request Issue Template +about: Suggest an idea for this project +title: "[FEATURE] OOO기능 구현" +labels: '' +assignees: '' + +--- + +## 📝 Issue +> 추가하려는 기능에 대해 설명해 주세요. + +--- + +## 📄 설명 + +- 기능 +- 기능 +- 기능 + +--- + +## ✅ 작업할 내용 +- [ ] 할일 +- [ ] 할일 +- [ ] 할일 +- [ ] 할일 + +--- + +## 🙋🏻 참고 자료 +> 관련 문서나 참고 링크가 있다면 적어주세요. From 639d1c495bb75dc074806f01be1e2eac6ad386c8 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:57:41 +0900 Subject: [PATCH 008/164] Delete .github/ISSUE_TEMPLATE directory --- .github/ISSUE_TEMPLATE/bug_report.yml | 35 ------------------- .../feature-request-issue-template.md | 32 ----------------- .github/ISSUE_TEMPLATE/feature_request.yml | 34 ------------------ 3 files changed, 101 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request-issue-template.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 4b3c320..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "🐞 Bug Report" -description: "버그 신고용 템플릿입니다." -labels: ["bug"] -body: - - type: textarea - attributes: - label: 📄 버그 설명 - description: 어떤 문제가 발생했는지 자세히 적어주세요. - placeholder: 예시) 로그인 버튼을 눌렀는데 반응이 없습니다. - validations: - required: true - - - type: textarea - attributes: - label: 🪛 재현 방법 - description: 버그를 어떻게 재현할 수 있는지 단계별로 알려주세요. - placeholder: | - 1. 페이지 접속 - 2. 로그인 버튼 클릭 - 3. 아무 반응 없음 - validations: - required: true - - - type: textarea - attributes: - label: ✅ 기대 동작 - description: 원래 어떻게 동작했어야 하는지 작성해주세요. - placeholder: 로그인 성공 후 대시보드로 이동해야 합니다. - validations: - required: true - - - type: textarea - attributes: - label: 📎 참고 자료 (스크린샷 등) - description: 필요하다면 이미지, 로그 등 추가해주세요. diff --git a/.github/ISSUE_TEMPLATE/feature-request-issue-template.md b/.github/ISSUE_TEMPLATE/feature-request-issue-template.md deleted file mode 100644 index 13806a3..0000000 --- a/.github/ISSUE_TEMPLATE/feature-request-issue-template.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Feature Request Issue Template -about: Suggest an idea for this project -title: "[FEATURE] OOO기능 구현" -labels: '' -assignees: '' - ---- - -## 📝 Issue -> 추가하려는 기능에 대해 설명해 주세요. - ---- - -## 📄 설명 - -- 기능 -- 기능 -- 기능 - ---- - -## ✅ 작업할 내용 -- [ ] 할일 -- [ ] 할일 -- [ ] 할일 -- [ ] 할일 - ---- - -## 🙋🏻 참고 자료 -> 관련 문서나 참고 링크가 있다면 적어주세요. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index d39de6f..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "✨ Feature" -description: "새로운 기능 추가용 템플릿입니다." -labels: ["feature"] -body: - - type: textarea - attributes: - label: 📄 설명 - description: 새로운 기능에 대한 설명을 작성해 주세요. 아래는 제목 작성 예시입니다. - ``` - [FEATURE] 사용자 로그인 기능 추가 - [BUG] 로그인 실패 시 에러 발생 - ``` - 제목은 위 형식 참고해서 작성해 주세요! - placeholder: 자세히 적을수록 좋습니다. - validations: - required: true - - - type: textarea - attributes: - label: ✅ 작업할 내용 - description: 할 일을 체크박스 형태로 작성해주세요. - value: | - - [ ] 작업 내용 - - [ ] 작업 내용 - - [ ] 작업 내용 - - [ ] 작업 내용 - render: markdown - validations: - required: true - - - type: textarea - attributes: - label: 🙋🏻 참고 자료 - description: 참고 자료가 있다면 작성해 주세요. From 2688e87b9d3f0e4a92724b8d556f45037e497222 Mon Sep 17 00:00:00 2001 From: euphony <86907076+3uomlkh@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:04:22 +0900 Subject: [PATCH 009/164] Create feature-request-issue-template.md --- .../feature-request-issue-template.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature-request-issue-template.md diff --git a/.github/ISSUE_TEMPLATE/feature-request-issue-template.md b/.github/ISSUE_TEMPLATE/feature-request-issue-template.md new file mode 100644 index 0000000..13806a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request-issue-template.md @@ -0,0 +1,32 @@ +--- +name: Feature Request Issue Template +about: Suggest an idea for this project +title: "[FEATURE] OOO기능 구현" +labels: '' +assignees: '' + +--- + +## 📝 Issue +> 추가하려는 기능에 대해 설명해 주세요. + +--- + +## 📄 설명 + +- 기능 +- 기능 +- 기능 + +--- + +## ✅ 작업할 내용 +- [ ] 할일 +- [ ] 할일 +- [ ] 할일 +- [ ] 할일 + +--- + +## 🙋🏻 참고 자료 +> 관련 문서나 참고 링크가 있다면 적어주세요. From c617c99ac3559c82c77acd1c51339f5f89f92fb1 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 00:26:57 +0900 Subject: [PATCH 010/164] =?UTF-8?q?feat(auth):=20Spring=20Security=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - filter와 arguement resolver를 spring security를 사용해서 구현 - 토큰에는 userId, email, nickname, userRole을 담음 - Access Token 유효기간은 10분, 비교적 짧게하는 것이 보안상 좋음 - Refresh Token 유효기간은 1~2주로 할 예정 랜덤값 - JwtAuthenticationToken에서 getPrincipal 사용 - @AuthenticationPrincipal AuthUser authUser 사용 가능 ### 수정한 내용 - build.gradle: jwt를 구현하기 위한 의존성 추가 --- build.gradle | 8 +- .../config/JwtAuthenticationFilter.java | 78 +++++++++++++++++++ .../global/config/JwtAuthenticationToken.java | 25 ++++++ .../eightyage/global/config/JwtUtil.java | 67 ++++++++++++++++ .../global/config/SecurityConfig.java | 48 ++++++++++++ .../eightyage/global/dto/AuthUser.java | 27 +++++++ 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/eightyage/global/config/JwtAuthenticationToken.java create mode 100644 src/main/java/com/example/eightyage/global/config/JwtUtil.java create mode 100644 src/main/java/com/example/eightyage/global/config/SecurityConfig.java create mode 100644 src/main/java/com/example/eightyage/global/dto/AuthUser.java diff --git a/build.gradle b/build.gradle index 2bc6c71..1524b0a 100644 --- a/build.gradle +++ b/build.gradle @@ -32,12 +32,18 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // spring security - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + // jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + // spring cloud AWS S3 // implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' } diff --git a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..65882a6 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java @@ -0,0 +1,78 @@ +package com.example.eightyage.global.config; + +import com.example.eightyage.domain.auth.entity.UserRole; +import com.example.eightyage.global.dto.AuthUser; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String jwt = jwtUtil.substringToken(authorizationHeader); + + try { + Claims claims = jwtUtil.extractClaims(jwt); + + if (SecurityContextHolder.getContext().getAuthentication() == null) { + setAuthentication(claims); + } + + } catch (SecurityException | MalformedJwtException e) { + log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); + return; + } catch (ExpiredJwtException e) { + log.error("Expired JWT token, 만료된 JWT token 입니다.", e); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); + return; + } catch (UnsupportedJwtException e) { + log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); + return; + } catch (Exception e) { + log.error("Internal server error", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + } + filterChain.doFilter(request, response); + } + + private void setAuthentication(Claims claims) { + Long userId = Long.valueOf(claims.getSubject()); + String email = claims.get("email", String.class); + String nickname = claims.get("nickname", String.class); + UserRole userRole = UserRole.of(claims.get("userRole", String.class)); + + AuthUser authUser = new AuthUser(userId, email, nickname, userRole); + JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationToken.java b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationToken.java new file mode 100644 index 0000000..b69aefa --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationToken.java @@ -0,0 +1,25 @@ +package com.example.eightyage.global.config; + +import com.example.eightyage.global.dto.AuthUser; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + + private final AuthUser authUser; + + public JwtAuthenticationToken(AuthUser authUser) { + super(authUser.getAuthorities()); + this.authUser = authUser; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return authUser; + } +} diff --git a/src/main/java/com/example/eightyage/global/config/JwtUtil.java b/src/main/java/com/example/eightyage/global/config/JwtUtil.java new file mode 100644 index 0000000..1192e94 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/JwtUtil.java @@ -0,0 +1,67 @@ +package com.example.eightyage.global.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import com.example.eightyage.domain.auth.entity.UserRole; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.rmi.ServerException; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Slf4j(topic = "JwtUtil") +@Component +public class JwtUtil { + + private static final String BEARER_PREFIX = "Bearer "; + private static final long ACCESS_TOKEN_TIME = 10 * 60 * 1000L; // 10분 + + @Value("${jwt.secret.key}") + private String secretKey; + private Key key; + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + @PostConstruct + public void init() { + byte[] bytes = Base64.getDecoder().decode(secretKey); + key = Keys.hmacShaKeyFor(bytes); + } + + public String createToken(Long userId, String email, String nickname, UserRole userRole) { + Date date = new Date(); + + return BEARER_PREFIX + + Jwts.builder() + .setSubject(String.valueOf(userId)) + .claim("email", email) + .claim("nickname", nickname) + .claim("userRole", userRole) + .setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME)) + .setIssuedAt(date) + .signWith(key, signatureAlgorithm) + .compact(); + } + + public String substringToken(String tokenValue) throws ServerException { + if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { + return tokenValue.substring(7); + } + throw new ServerException("Not Found Token"); + } + + public Claims extractClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } +} + diff --git a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java new file mode 100644 index 0000000..a402e8b --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java @@ -0,0 +1,48 @@ +package com.example.eightyage.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class) + .formLogin(AbstractHttpConfigurer::disable) + .anonymous(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .rememberMe(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll() + .anyRequest().authenticated() + ) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/global/dto/AuthUser.java b/src/main/java/com/example/eightyage/global/dto/AuthUser.java new file mode 100644 index 0000000..20bc055 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/dto/AuthUser.java @@ -0,0 +1,27 @@ +package com.example.eightyage.global.dto; + +import com.example.eightyage.domain.auth.entity.UserRole; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collection; +import java.util.List; + +@Getter +@AllArgsConstructor +public class AuthUser { + + private final Long userId; + private final String email; + private final String nickname; + private final Collection authorities; + + public AuthUser(Long userId, String email, String nickname, UserRole role) { + this.userId = userId; + this.email = email; + this.nickname = nickname; + this.authorities = List.of(new SimpleGrantedAuthority(role.name())); + } +} \ No newline at end of file From 1d6ab249a74e32d97adab729cc73ce3dd1b2409a Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 03:12:33 +0900 Subject: [PATCH 011/164] =?UTF-8?q?feat(auth):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - refreshToken을 저장하기 위해 RefreshToken엔티티 구현 - HTTP Only 방식을 사용하여 refreshToken을 캐시에 저장(UUID를 이용한 랜덤값) - Refresh Token 유효기간은 2주 ### 의논할 것 - 유저가 ADMIN으로 가입하는 것을 방지하기 위해 request에 유저 ROLE기입을 하지 않음 - 가입할 때 자동으로 USER 권한으로 가입 - ADMIM 권한으로 가입하고 싶은 경우 따로 INSERT 쿼리를 작성하거나 어드민 가입 api를 따로 팔 것 - user Repository는 UserService만 관리하게 하려고 도메인을 user와 auth로 나눔 --- .../auth/controller/AuthController.java | 48 +++++++++++++++ .../domain/auth/dto/request/.gitkeep | 0 .../dto/request/AuthSignupRequestDto.java | 24 ++++++++ .../domain/auth/dto/response/.gitkeep | 0 .../response/AuthAccessTokenResponseDto.java | 12 ++++ .../dto/response/AuthTokensResponseDto.java | 13 ++++ .../domain/auth/entity/RefreshToken.java | 34 +++++++++++ .../domain/auth/entity/TokenState.java | 6 ++ .../eightyage/domain/auth/repository/.gitkeep | 0 .../repository/RefreshTokenRepository.java | 7 +++ .../eightyage/domain/auth/service/.gitkeep | 0 .../domain/auth/service/AuthService.java | 33 ++++++++++ .../domain/auth/service/TokenService.java | 61 +++++++++++++++++++ .../domain/{auth => user}/entity/User.java | 8 ++- .../{auth => user}/entity/UserRole.java | 2 +- .../user/repository/UserRepository.java | 8 +++ .../domain/user/service/UserService.java | 29 +++++++++ .../config/JwtAuthenticationFilter.java | 2 +- .../eightyage/global/config/JwtUtil.java | 4 +- .../global/config/SecurityConfig.java | 2 +- .../eightyage/global/dto/AuthUser.java | 2 +- src/main/resources/application.yml | 3 +- 22 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java delete mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/request/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/response/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java create mode 100644 src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java delete mode 100644 src/main/java/com/example/eightyage/domain/auth/repository/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java delete mode 100644 src/main/java/com/example/eightyage/domain/auth/service/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/auth/service/AuthService.java create mode 100644 src/main/java/com/example/eightyage/domain/auth/service/TokenService.java rename src/main/java/com/example/eightyage/domain/{auth => user}/entity/User.java (64%) rename src/main/java/com/example/eightyage/domain/{auth => user}/entity/UserRole.java (93%) create mode 100644 src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/user/service/UserService.java diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..9b5e691 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -0,0 +1,48 @@ +package com.example.eightyage.domain.auth.controller; + +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.auth.service.AuthService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class AuthController { + + private final AuthService authService; + private static final int REFRESH_TOKEN_TIME = 7 * 24 * 60 * 60; // 7주일 + + /* 회원가입 */ + @PostMapping("/v1/auth/signup") + public AuthAccessTokenResponseDto signup( + @RequestBody AuthSignupRequestDto request, + HttpServletResponse httpServletResponse + ) { + AuthTokensResponseDto tokensResponseDto = authService.signup(request); + + setRefreshTokenCookie(httpServletResponse, tokensResponseDto.getRefreshToken()); + + return new AuthAccessTokenResponseDto(tokensResponseDto.getAccessToken()); + } + + /* http only 사용하기 위해 쿠키에 refreshToken 저장 */ + private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + Cookie cookie = new Cookie("refreshToken", refreshToken); + cookie.setMaxAge(REFRESH_TOKEN_TIME); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setPath("/"); + + response.addCookie(cookie); + } + + +} diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java new file mode 100644 index 0000000..0defb39 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -0,0 +1,24 @@ +package com.example.eightyage.domain.auth.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; + +@Getter +public class AuthSignupRequestDto { + + @NotBlank(message = "이메일은 필수 입력 값입니다.") + @Email(message = "이메일 형식으로 입력되어야 합니다.") + private String email; + + @NotBlank(message = "닉네임은 필수 입력 값입니다.") + private String nickname; + + @NotBlank(message = "비밀번호는 필수 입력 값입니다.") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", + message = "비밀번호는 영어, 숫자 포함 8자리 이상이어야 합니다.") + private String password; + + private String passwordCheck; +} diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/dto/response/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java new file mode 100644 index 0000000..660a03d --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java @@ -0,0 +1,12 @@ +package com.example.eightyage.domain.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AuthAccessTokenResponseDto { + + private final String accessToken; + +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java new file mode 100644 index 0000000..a7a77b3 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java @@ -0,0 +1,13 @@ +package com.example.eightyage.domain.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AuthTokensResponseDto { + + private final String AccessToken; + private final String refreshToken; + +} diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java new file mode 100644 index 0000000..2d7e85f --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java @@ -0,0 +1,34 @@ +package com.example.eightyage.domain.auth.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private String RefreshToken; + + @Enumerated(EnumType.STRING) + private TokenState tokenState; + + public RefreshToken(Long userId) { + this.userId = userId; + this.RefreshToken = UUID.randomUUID().toString(); + this.tokenState = TokenState.VALID; + } + + public void updateTokenStatus(TokenState tokenStatus){ + this.tokenState = tokenStatus; + } +} diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java b/src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java new file mode 100644 index 0000000..3ec0bd7 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.auth.entity; + +public enum TokenState { + VALID, + INVALIDATED +} diff --git a/src/main/java/com/example/eightyage/domain/auth/repository/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..685a6ef --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package com.example.eightyage.domain.auth.repository; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository{ +} diff --git a/src/main/java/com/example/eightyage/domain/auth/service/.gitkeep b/src/main/java/com/example/eightyage/domain/auth/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java new file mode 100644 index 0000000..6b3dbc3 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -0,0 +1,33 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserService userService; + private final TokenService tokenService; + + /* 회원가입 */ + public AuthTokensResponseDto signup(AuthSignupRequestDto request) { + + if (!request.getPassword().equals(request.getPasswordCheck())) { + throw new BadRequestException("비밀번호 확인을 입력해주세요"); + } + + User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); + + String accessToken = tokenService.createAccessToken(user); + String refreshToken = tokenService.createRefreshToken(user); + + return new AuthTokensResponseDto(accessToken, refreshToken); + } +} diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java new file mode 100644 index 0000000..d505ce1 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -0,0 +1,61 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import com.example.eightyage.domain.auth.repository.RefreshTokenRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.config.JwtUtil; +import com.example.eightyage.global.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; + +@Service +@RequiredArgsConstructor +public class TokenService { + + private final RefreshTokenRepository refreshTokenRepository; + private final UserService userService; + private final JwtUtil jwtUtil; + + /* Access Token 생성 */ + public String createAccessToken(User user) { + return jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole()); + } + + /* Refresh Token 생성 */ + public String createRefreshToken(User user) { + RefreshToken refreshToken = refreshTokenRepository.save(new RefreshToken(user.getId())); + return refreshToken.getRefreshToken(); + } + +// /* Refresh Token 만료 */ +// public void revokeRefreshToken(Long userId) { +// RefreshToken refreshToken = findRefreshTokenById(userId); +// refreshToken.updateTokenStatus(INVALIDATED); +// } +// +// /* Refresh Token 유효성 검사 */ +// public User reissueToken(String token) { +// +// RefreshToken refreshToken = findByTokenOrElseThrow(token); +// +// if (refreshToken.getTokenState() == INVALIDATED) { +// throw new UnauthorizedException("사용이 만료된 refresh token 입니다."); +// } +// refreshToken.updateTokenStatus(INVALIDATED); +// +// return userService.findUserByIdOrElseThrow(refreshToken.getUserId()); +// } +// +// private RefreshToken findByTokenOrElseThrow(String token) { +// return refreshTokenRepository.findByToken(token).orElseThrow( +// () -> new NotFoundException("Not Found Token")); +// } +// +// private RefreshToken findRefreshTokenById(Long userId) { +// return refreshTokenRepository.findById(userId).orElseThrow( +// () -> new NotFoundException("Not Found Token")); +// } +} diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java similarity index 64% rename from src/main/java/com/example/eightyage/domain/auth/entity/User.java rename to src/main/java/com/example/eightyage/domain/user/entity/User.java index 27c1cf5..bc109b4 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.auth.entity; +package com.example.eightyage.domain.user.entity; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; @@ -24,4 +24,10 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; + public User(String email, String nickname, String password) { + this.email = email; + this.nickname = nickname; + this.password = password; + this.userRole = UserRole.ROLE_USER; + } } diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java similarity index 93% rename from src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java rename to src/main/java/com/example/eightyage/domain/user/entity/UserRole.java index 1f23602..c384dd2 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/UserRole.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.auth.entity; +package com.example.eightyage.domain.user.entity; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..05757ee --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java @@ -0,0 +1,8 @@ +package com.example.eightyage.domain.user.repository; + +import com.example.eightyage.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); +} diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java new file mode 100644 index 0000000..66eaa5b --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.user.service; + +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public User saveUser(String email, String nickname, String password) { + + if (userRepository.existsByEmail(email)) { + throw new BadRequestException("등록된 이메일입니다."); + } + + String encodedPassword = passwordEncoder.encode(password); + + User user = new User(email, nickname, encodedPassword); + + return userRepository.save(user); + } +} diff --git a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java index 65882a6..205f6f4 100644 --- a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.config; -import com.example.eightyage.domain.auth.entity.UserRole; +import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.global.dto.AuthUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; diff --git a/src/main/java/com/example/eightyage/global/config/JwtUtil.java b/src/main/java/com/example/eightyage/global/config/JwtUtil.java index 1192e94..26620a0 100644 --- a/src/main/java/com/example/eightyage/global/config/JwtUtil.java +++ b/src/main/java/com/example/eightyage/global/config/JwtUtil.java @@ -4,7 +4,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import com.example.eightyage.domain.auth.entity.UserRole; +import com.example.eightyage.domain.user.entity.UserRole; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -34,7 +34,7 @@ public void init() { key = Keys.hmacShaKeyFor(bytes); } - public String createToken(Long userId, String email, String nickname, UserRole userRole) { + public String createAccessToken(Long userId, String email, String nickname, UserRole userRole) { Date date = new Date(); return BEARER_PREFIX + diff --git a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java index a402e8b..1d3bb61 100644 --- a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java +++ b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java @@ -40,7 +40,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logout(AbstractHttpConfigurer::disable) .rememberMe(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll() + .requestMatchers(request -> request.getRequestURI().startsWith("/api/v1/auth")).permitAll() .anyRequest().authenticated() ) .build(); diff --git a/src/main/java/com/example/eightyage/global/dto/AuthUser.java b/src/main/java/com/example/eightyage/global/dto/AuthUser.java index 20bc055..8785cfe 100644 --- a/src/main/java/com/example/eightyage/global/dto/AuthUser.java +++ b/src/main/java/com/example/eightyage/global/dto/AuthUser.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.dto; -import com.example.eightyage.domain.auth.entity.UserRole; +import com.example.eightyage.domain.user.entity.UserRole; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 99dc2c4..e592d69 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,4 +30,5 @@ spring: dialect: org.hibernate.dialect.MySQLDialect jwt: - secret: ${JWT_SECRET_KEY} + secret: + key: ${JWT_SECRET_KEY} From 13aefd511ef367f409658df383b67e2d08f421cf Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 03:35:23 +0900 Subject: [PATCH 012/164] =?UTF-8?q?feat(auth):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - 로그인서비스를 authService에 구현하고, User에 접근하는 서비스를 의존함 - 로그인을 한 후 응답으로는 AccessToken을, 쿠키에는 RefreshToken을 전달 --- .../auth/controller/AuthController.java | 23 +++++++++++++++---- .../dto/request/AuthSigninRequestDto.java | 11 +++++++++ .../domain/auth/service/AuthService.java | 23 +++++++++++++++++++ .../user/repository/UserRepository.java | 4 ++++ .../domain/user/service/UserService.java | 7 ++++++ 5 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index 9b5e691..0e8b70a 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -1,16 +1,16 @@ package com.example.eightyage.domain.auth.controller; +import com.example.eightyage.domain.auth.dto.request.AuthSigninRequestDto; import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; import com.example.eightyage.domain.auth.service.AuthService; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.Getter; import lombok.RequiredArgsConstructor; -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 org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -23,7 +23,7 @@ public class AuthController { /* 회원가입 */ @PostMapping("/v1/auth/signup") public AuthAccessTokenResponseDto signup( - @RequestBody AuthSignupRequestDto request, + @Valid @RequestBody AuthSignupRequestDto request, HttpServletResponse httpServletResponse ) { AuthTokensResponseDto tokensResponseDto = authService.signup(request); @@ -33,6 +33,19 @@ public AuthAccessTokenResponseDto signup( return new AuthAccessTokenResponseDto(tokensResponseDto.getAccessToken()); } + /* 로그인 */ + @PostMapping("/v1/auth/signin") + public AuthAccessTokenResponseDto signin( + @Valid @RequestBody AuthSigninRequestDto request, + HttpServletResponse httpServletResponse + ) { + AuthTokensResponseDto tokensResponseDto = authService.signin(request); + + setRefreshTokenCookie(httpServletResponse, tokensResponseDto.getRefreshToken()); + + return new AuthAccessTokenResponseDto(tokensResponseDto.getAccessToken()); + } + /* http only 사용하기 위해 쿠키에 refreshToken 저장 */ private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { Cookie cookie = new Cookie("refreshToken", refreshToken); diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java new file mode 100644 index 0000000..3b08a29 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java @@ -0,0 +1,11 @@ +package com.example.eightyage.domain.auth.dto.request; + +import lombok.Getter; + +@Getter +public class AuthSigninRequestDto { + + private String email; + private String password; + +} diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 6b3dbc3..be09061 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -1,13 +1,17 @@ package com.example.eightyage.domain.auth.service; +import com.example.eightyage.domain.auth.dto.request.AuthSigninRequestDto; import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -15,6 +19,7 @@ public class AuthService { private final UserService userService; private final TokenService tokenService; + private final PasswordEncoder passwordEncoder; /* 회원가입 */ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { @@ -25,6 +30,24 @@ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); + return getTokenResponse(user); + } + + /* 로그인 */ + @Transactional + public AuthTokensResponseDto signin(AuthSigninRequestDto request) { + User user = userService.findUserByEmailOrElseThrow(request.getEmail()); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + throw new UnauthorizedException("잘못된 비밀번호입니다."); + } + + return getTokenResponse(user); + } + + /* Access Token, Refresh Token 생성 및 저장 */ + private AuthTokensResponseDto getTokenResponse(User user) { + String accessToken = tokenService.createAccessToken(user); String refreshToken = tokenService.createRefreshToken(user); diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java index 05757ee..6ab9a06 100644 --- a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java @@ -3,6 +3,10 @@ import com.example.eightyage.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); + + Optional findByEmail(String email); } diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 66eaa5b..54cf4be 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -26,4 +27,10 @@ public User saveUser(String email, String nickname, String password) { return userRepository.save(user); } + + public User findUserByEmailOrElseThrow(String email) { + return userRepository.findByEmail(email).orElseThrow( + () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") + ); + } } From ce23291922d36371a2a1780ec94a1fd9e5ab6269 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 03:55:40 +0900 Subject: [PATCH 013/164] =?UTF-8?q?feat(auth):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - argument resolver를 사용하여 RefreshToken을 header의 쿠키에서 가져올 수 있도록 구현 - token rotation방법으로 토큰 재발급 --- .../auth/controller/AuthController.java | 14 ++++++ .../repository/RefreshTokenRepository.java | 5 +- .../domain/auth/service/AuthService.java | 8 ++++ .../domain/auth/service/TokenService.java | 48 ++++++++----------- .../domain/user/service/UserService.java | 7 +++ .../global/annotation/RefreshToken.java | 11 +++++ .../argument/RefreshArgumentResolver.java | 44 +++++++++++++++++ .../eightyage/global/config/WebConfig.java | 18 +++++++ 8 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/annotation/RefreshToken.java create mode 100644 src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java create mode 100644 src/main/java/com/example/eightyage/global/config/WebConfig.java diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index 0e8b70a..5fe7aba 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -5,6 +5,7 @@ import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; import com.example.eightyage.domain.auth.service.AuthService; +import com.example.eightyage.global.annotation.RefreshToken; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -46,6 +47,19 @@ public AuthAccessTokenResponseDto signin( return new AuthAccessTokenResponseDto(tokensResponseDto.getAccessToken()); } + /* 토큰 재발급 (로그인 기간 연장) */ + @GetMapping("/v1/auth/refresh") + public AuthAccessTokenResponseDto refresh( + @RefreshToken String refreshToken, + HttpServletResponse httpServletResponse + ) { + AuthTokensResponseDto tokensResponseDto = authService.reissueAccessToken(refreshToken); + + setRefreshTokenCookie(httpServletResponse, tokensResponseDto.getRefreshToken()); + + return new AuthAccessTokenResponseDto(tokensResponseDto.getAccessToken()); + } + /* http only 사용하기 위해 쿠키에 refreshToken 저장 */ private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { Cookie cookie = new Cookie("refreshToken", refreshToken); diff --git a/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java index 685a6ef..9f7bb9f 100644 --- a/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java +++ b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java @@ -3,5 +3,8 @@ import com.example.eightyage.domain.auth.entity.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface RefreshTokenRepository extends JpaRepository{ -} + Optional findByRefreshToken(String token); +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index be09061..71d5566 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -45,6 +45,14 @@ public AuthTokensResponseDto signin(AuthSigninRequestDto request) { return getTokenResponse(user); } + /* Access Token, Refresh Token 재발급 */ + @Transactional + public AuthTokensResponseDto reissueAccessToken(String refreshToken) { + User user = tokenService.reissueToken(refreshToken); + + return getTokenResponse(user); + } + /* Access Token, Refresh Token 생성 및 저장 */ private AuthTokensResponseDto getTokenResponse(User user) { diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java index d505ce1..cc7fcf6 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -5,6 +5,7 @@ import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.config.JwtUtil; +import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -30,32 +31,21 @@ public String createRefreshToken(User user) { return refreshToken.getRefreshToken(); } -// /* Refresh Token 만료 */ -// public void revokeRefreshToken(Long userId) { -// RefreshToken refreshToken = findRefreshTokenById(userId); -// refreshToken.updateTokenStatus(INVALIDATED); -// } -// -// /* Refresh Token 유효성 검사 */ -// public User reissueToken(String token) { -// -// RefreshToken refreshToken = findByTokenOrElseThrow(token); -// -// if (refreshToken.getTokenState() == INVALIDATED) { -// throw new UnauthorizedException("사용이 만료된 refresh token 입니다."); -// } -// refreshToken.updateTokenStatus(INVALIDATED); -// -// return userService.findUserByIdOrElseThrow(refreshToken.getUserId()); -// } -// -// private RefreshToken findByTokenOrElseThrow(String token) { -// return refreshTokenRepository.findByToken(token).orElseThrow( -// () -> new NotFoundException("Not Found Token")); -// } -// -// private RefreshToken findRefreshTokenById(Long userId) { -// return refreshTokenRepository.findById(userId).orElseThrow( -// () -> new NotFoundException("Not Found Token")); -// } -} + /* Refresh Token 유효성 검사 */ + public User reissueToken(String token) { + + RefreshToken refreshToken = findByTokenOrElseThrow(token); + + if (refreshToken.getTokenState() == INVALIDATED) { + throw new UnauthorizedException("사용이 만료된 refresh token 입니다."); + } + refreshToken.updateTokenStatus(INVALIDATED); + + return userService.findUserByIdOrElseThrow(refreshToken.getUserId()); + } + + private RefreshToken findByTokenOrElseThrow(String token) { + return refreshTokenRepository.findByRefreshToken(token).orElseThrow( + () -> new NotFoundException("리프레시 토큰을 찾을 수 없습니다.")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 54cf4be..fec30fc 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -33,4 +34,10 @@ public User findUserByEmailOrElseThrow(String email) { () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") ); } + + public User findUserByIdOrElseThrow(Long userId) { + return userRepository.findById(userId).orElseThrow( + () -> new NotFoundException("해당 유저의 Id를 찾을 수 없습니다.") + ); + } } diff --git a/src/main/java/com/example/eightyage/global/annotation/RefreshToken.java b/src/main/java/com/example/eightyage/global/annotation/RefreshToken.java new file mode 100644 index 0000000..537610d --- /dev/null +++ b/src/main/java/com/example/eightyage/global/annotation/RefreshToken.java @@ -0,0 +1,11 @@ +package com.example.eightyage.global.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface RefreshToken { +} diff --git a/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java new file mode 100644 index 0000000..5a5be23 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java @@ -0,0 +1,44 @@ +package com.example.eightyage.global.argument; + +import com.example.eightyage.global.annotation.RefreshToken; +import com.example.eightyage.global.exception.UnauthorizedException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +public class RefreshArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasRefreshTokenAnnotation = parameter.getParameterAnnotation(RefreshToken.class) != null; + boolean isStringType = parameter.getParameterType().equals(String.class); + + if (hasRefreshTokenAnnotation != isStringType) { + throw new UnauthorizedException("@RefreshToken과 String 타입은 함께 사용되어야 합니다."); + } + return hasRefreshTokenAnnotation; + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("refreshToken".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + throw new UnauthorizedException("리프레시 토큰이 존재하지 않습니다. 다시 로그인 해주세요."); + } +} diff --git a/src/main/java/com/example/eightyage/global/config/WebConfig.java b/src/main/java/com/example/eightyage/global/config/WebConfig.java new file mode 100644 index 0000000..e07060f --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.example.eightyage.global.config; + +import com.example.eightyage.global.argument.RefreshArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + /* ArgumentResolver 등록 */ + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new RefreshArgumentResolver()); + } +} \ No newline at end of file From 3e88571817dc0c0dac9f5e4caa2b3a6c87090b17 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 04:18:40 +0900 Subject: [PATCH 014/164] =?UTF-8?q?refactor(auth):=20RefreshToken=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=AA=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=EA=B2=80=EC=82=AC=EC=8B=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - RefreshToken이라는 엔티티에 RefreshToken이라는 필드가 있어 혼동하지 않게 수정 - 에러메세지가 여러개 출력되도록 제네릭 사용 --- .../auth/controller/AuthController.java | 1 - .../domain/auth/entity/RefreshToken.java | 4 ++-- .../repository/RefreshTokenRepository.java | 2 +- .../domain/auth/service/AuthService.java | 1 - .../domain/auth/service/TokenService.java | 6 ++--- .../global/config/SecurityConfig.java | 1 + .../global/entity/ErrorResponse.java | 10 ++++---- .../exception/GlobalExceptionHandler.java | 24 ++++++++++++++++++- .../example/eightyage/global/filter/.gitkeep | 0 .../JwtAuthenticationFilter.java | 4 +++- .../global/{config => util}/JwtUtil.java | 2 +- 11 files changed, 39 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/global/filter/.gitkeep rename src/main/java/com/example/eightyage/global/{config => filter}/JwtAuthenticationFilter.java (95%) rename src/main/java/com/example/eightyage/global/{config => util}/JwtUtil.java (97%) diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index 5fe7aba..aec6a97 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -9,7 +9,6 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; -import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java index 2d7e85f..0c1c3c1 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java +++ b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java @@ -17,14 +17,14 @@ public class RefreshToken { private Long userId; - private String RefreshToken; + private String token; @Enumerated(EnumType.STRING) private TokenState tokenState; public RefreshToken(Long userId) { this.userId = userId; - this.RefreshToken = UUID.randomUUID().toString(); + this.token = UUID.randomUUID().toString(); this.tokenState = TokenState.VALID; } diff --git a/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java index 9f7bb9f..624e2b8 100644 --- a/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java +++ b/src/main/java/com/example/eightyage/domain/auth/repository/RefreshTokenRepository.java @@ -6,5 +6,5 @@ import java.util.Optional; public interface RefreshTokenRepository extends JpaRepository{ - Optional findByRefreshToken(String token); + Optional findByToken(String token); } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 71d5566..d69456b 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -2,7 +2,6 @@ import com.example.eightyage.domain.auth.dto.request.AuthSigninRequestDto; import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; -import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java index cc7fcf6..6483524 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -4,7 +4,7 @@ import com.example.eightyage.domain.auth.repository.RefreshTokenRepository; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; -import com.example.eightyage.global.config.JwtUtil; +import com.example.eightyage.global.util.JwtUtil; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; @@ -28,7 +28,7 @@ public String createAccessToken(User user) { /* Refresh Token 생성 */ public String createRefreshToken(User user) { RefreshToken refreshToken = refreshTokenRepository.save(new RefreshToken(user.getId())); - return refreshToken.getRefreshToken(); + return refreshToken.getToken(); } /* Refresh Token 유효성 검사 */ @@ -45,7 +45,7 @@ public User reissueToken(String token) { } private RefreshToken findByTokenOrElseThrow(String token) { - return refreshTokenRepository.findByRefreshToken(token).orElseThrow( + return refreshTokenRepository.findByToken(token).orElseThrow( () -> new NotFoundException("리프레시 토큰을 찾을 수 없습니다.")); } } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java index 1d3bb61..2fe9756 100644 --- a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java +++ b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.example.eightyage.global.config; +import com.example.eightyage.global.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java b/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java index 42e7332..6d9a51d 100644 --- a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java +++ b/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java @@ -8,21 +8,21 @@ @Getter @Setter @NoArgsConstructor -public class ErrorResponse { +public class ErrorResponse { private String status; private Integer code; - private String message; + private T message; - public ErrorResponse(HttpStatus httpStatus, String message) { + public ErrorResponse(HttpStatus httpStatus, T message) { this.status = httpStatus.name(); this.code = httpStatus.value(); this.message = message; } - public static ErrorResponse of(HttpStatus httpStatus, String message) { - return new ErrorResponse(httpStatus, message); + public static ErrorResponse of(HttpStatus httpStatus, T message) { + return new ErrorResponse<>(httpStatus, message); } } diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 1ca08b0..d297f34 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -1,16 +1,38 @@ package com.example.eightyage.global.exception; import com.example.eightyage.global.entity.ErrorResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.List; + +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) - public ErrorResponse invalidRequestExceptionException(CustomException ex) { + public ErrorResponse invalidRequestExceptionException(CustomException ex) { HttpStatus httpStatus = ex.getHttpStatus(); return ErrorResponse.of(httpStatus, ex.getMessage()); } + + @ExceptionHandler + public ErrorResponse> handleValidationException(MethodArgumentNotValidException e) { + List fieldErrors = e.getBindingResult().getFieldErrors(); + + List validFailedList = fieldErrors.stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .toList(); + return ErrorResponse.of(HttpStatus.BAD_REQUEST, validFailedList); + } + + @ExceptionHandler(Exception.class) + public ErrorResponse handleGlobalException(Exception e) { + log.error("Exception : {}",e.getMessage(), e); + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."); + } } diff --git a/src/main/java/com/example/eightyage/global/filter/.gitkeep b/src/main/java/com/example/eightyage/global/filter/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java similarity index 95% rename from src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java rename to src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java index 205f6f4..3a8d43b 100644 --- a/src/main/java/com/example/eightyage/global/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java @@ -1,6 +1,8 @@ -package com.example.eightyage.global.config; +package com.example.eightyage.global.filter; import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.global.config.JwtAuthenticationToken; +import com.example.eightyage.global.util.JwtUtil; import com.example.eightyage.global.dto.AuthUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; diff --git a/src/main/java/com/example/eightyage/global/config/JwtUtil.java b/src/main/java/com/example/eightyage/global/util/JwtUtil.java similarity index 97% rename from src/main/java/com/example/eightyage/global/config/JwtUtil.java rename to src/main/java/com/example/eightyage/global/util/JwtUtil.java index 26620a0..9aea79f 100644 --- a/src/main/java/com/example/eightyage/global/config/JwtUtil.java +++ b/src/main/java/com/example/eightyage/global/util/JwtUtil.java @@ -1,4 +1,4 @@ -package com.example.eightyage.global.config; +package com.example.eightyage.global.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; From 6bf62bfd3c3735ab638220902f29d8a96d75b340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 11:46:21 +0900 Subject: [PATCH 015/164] =?UTF-8?q?feat:=20Product=20entity,=20Category=20?= =?UTF-8?q?enum,=20SaleState=20enum=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/entity/Category.java | 33 +++++++++++++++++++ .../domain/product/entity/Product.java | 31 +++++++++++++++++ .../domain/product/entity/SaleState.java | 6 ++++ 3 files changed, 70 insertions(+) create mode 100644 src/main/java/com/example/eightyage/domain/product/entity/Category.java create mode 100644 src/main/java/com/example/eightyage/domain/product/entity/Product.java create mode 100644 src/main/java/com/example/eightyage/domain/product/entity/SaleState.java diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Category.java b/src/main/java/com/example/eightyage/domain/product/entity/Category.java new file mode 100644 index 0000000..15c887f --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/entity/Category.java @@ -0,0 +1,33 @@ +package com.example.eightyage.domain.product.entity; + +public enum Category { + SKINCARE("스킨케어"), + MAKEUP("메이크업"), + HAIRCARE("헤어케어"), + BODYCARE("바디케어"), + FRAGRANCE("향수"), + SUNCARE("선케어"), + CLEANSING("클렌징"), + MASK_PACK("마스크팩"), + MEN_CARE("남성용"), + TOOL("뷰티 도구"), + + // 피부 타입 + DRY_SKIN("건성"), + OILY_SKIN("지성"), + NORMAL_SKIN("중성"), + COMBINATION_SKIN("복합성"), + SENSITIVE_SKIN("민감성"), + ACNE_PRONE_SKIN("여드름성"), + ATOPIC_SKIN("아토피"); + + private final String displayName; + + Category(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java new file mode 100644 index 0000000..d692eeb --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -0,0 +1,31 @@ +package com.example.eightyage.domain.product.entity; + +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +@Table(name="products") +public class Product extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private Category category; + + private String content; + + private Integer price; + + @Enumerated(EnumType.STRING) + private SaleState saleState; +} diff --git a/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java b/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java new file mode 100644 index 0000000..e69c789 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.product.entity; + +public enum SaleState { + FOR_SALE, + SOLD_OUT +} From dbecbc693554896018ee8dbb064b4afbf9092e51 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 12:03:09 +0900 Subject: [PATCH 016/164] =?UTF-8?q?chore(ci)=20:=20Gradle=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20=EA=B5=AC=EC=B6=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ci.yml 파일 생성 - PR 발생 시 자동으로 빌드 및 테스트가 실행되도록 구현 - gradle/gradle-build-action을 적용해 Gradle 캐시 활용 - JDK 17로 빌드 환경 구성 - 대상 브랜치는 dev, main으로 설정 --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2b849e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Java CI with Gradle + +on: + pull_request: + branches: [ "dev", "main" ] # dev, main 모두 PR 대상 + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build and Test with Gradle + uses: gradle/gradle-build-action@v3 + with: + arguments: clean build \ No newline at end of file From f9d0aa129db23d0a0969efda09f477fadea29263 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 13:45:15 +0900 Subject: [PATCH 017/164] =?UTF-8?q?fix(ci)=20:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - JWT_SECRET_KEY 설정 - gradle/gradle-build-action이 deprecated 되어 gradle/actions/setup-gradle로 수정 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b849e..89c1fe0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ permissions: jobs: build: runs-on: ubuntu-latest + env: + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} steps: - uses: actions/checkout@v4 @@ -21,6 +23,6 @@ jobs: distribution: 'temurin' - name: Build and Test with Gradle - uses: gradle/gradle-build-action@v3 + uses: gradle/actions/setup-gradle@v3 with: arguments: clean build \ No newline at end of file From ab439c3e118db037219ea2613dd9aa9e3c116171 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Tue, 25 Mar 2025 14:14:11 +0900 Subject: [PATCH 018/164] =?UTF-8?q?feat(search):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/EightyageApplication.java | 4 ++ .../controller/ProductController.java | 30 ++++++++++++++ .../dto/ProductSearchResponse.java | 27 +++++++++++++ .../search/fakeProduct/entity/Category.java | 6 +++ .../fakeProduct/entity/FakeProduct.java | 40 +++++++++++++++++++ .../search/fakeProduct/entity/SaleState.java | 6 +++ .../repository/ProductRepository.java | 23 +++++++++++ .../fakeProduct/service/ProductService.java | 27 +++++++++++++ 8 files changed, 163 insertions(+) create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java diff --git a/src/main/java/com/example/eightyage/EightyageApplication.java b/src/main/java/com/example/eightyage/EightyageApplication.java index 56da7c3..5b4e0a5 100644 --- a/src/main/java/com/example/eightyage/EightyageApplication.java +++ b/src/main/java/com/example/eightyage/EightyageApplication.java @@ -2,7 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; + +@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) @SpringBootApplication public class EightyageApplication { diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java new file mode 100644 index 0000000..7725ede --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java @@ -0,0 +1,30 @@ +package com.example.eightyage.domain.search.fakeProduct.controller; + +import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; +import com.example.eightyage.domain.search.fakeProduct.entity.Category; +import com.example.eightyage.domain.search.fakeProduct.service.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +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; + + +@RestController +@RequiredArgsConstructor +public class ProductController { + + private final ProductService productService; + + @GetMapping("/api/v1/products") + public ResponseEntity> searchProduct( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProducts(name, category, size, page)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java new file mode 100644 index 0000000..7f82943 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.search.fakeProduct.dto; + +import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ProductSearchResponse { + private final String productName; + private final String category; + private final Long price; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public static ProductSearchResponse from(FakeProduct fakeProduct) { + return ProductSearchResponse.builder() + .productName(fakeProduct.getName()) + .category(fakeProduct.getCategory().toString()) + .price(fakeProduct.getPrice()) + .createdAt(fakeProduct.getCreatedAt()) + .updatedAt(fakeProduct.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java new file mode 100644 index 0000000..f1323b0 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.search.fakeProduct.entity; + +public enum Category { + SKIN, + LOTION +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java new file mode 100644 index 0000000..f896682 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java @@ -0,0 +1,40 @@ +package com.example.eightyage.domain.search.fakeProduct.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor +@Getter +@EntityListeners(AuditingEntityListener.class) +public class FakeProduct { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Long id; + + private String name; + + @Enumerated(EnumType.STRING) + private Category category; + + private Long price; + + @Column(name = "sale_state") + @Enumerated(EnumType.STRING) + private SaleState saleState; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column + private LocalDateTime updatedAt; + +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java new file mode 100644 index 0000000..121b169 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.search.fakeProduct.entity; + +public enum SaleState { + ON_SALE, + NOT_SALE +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java new file mode 100644 index 0000000..fe28445 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java @@ -0,0 +1,23 @@ +package com.example.eightyage.domain.search.fakeProduct.repository; + +import com.example.eightyage.domain.search.fakeProduct.entity.Category; +import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ProductRepository extends JpaRepository { + + @Query("SELECT p FROM FakeProduct p WHERE p.saleState = 'ON_SALE' " + + "AND (:category IS NULL OR p.category = :category) " + + "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + + "ORDER BY p.name") + Page findProducts( + @Param("name")String name, + @Param("category") Category category, + Pageable pageable + ); + +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java new file mode 100644 index 0000000..f039ed8 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.search.fakeProduct.service; + +import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; +import com.example.eightyage.domain.search.fakeProduct.entity.Category; +import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; +import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + @Transactional(readOnly = true) + public Page getProducts(String productName, Category category, int size, int page) { + int adjustedPage = (page > 0) ? page - 1 : 0; + Pageable pageable = PageRequest.of(adjustedPage, size); + Page products = productRepository.findProducts(productName, category, pageable); + return products.map(ProductSearchResponse::from); + } +} \ No newline at end of file From 683bc60876dd1c2fca67c2816359e3f6b3036e52 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 14:55:33 +0900 Subject: [PATCH 019/164] =?UTF-8?q?fix(ci)=20:=20CI=EC=9A=A9=20application?= =?UTF-8?q?-ci.yml=20=EC=B6=94=EA=B0=80=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 추가 - Docker MySQL 사용하도록 수정 --- .github/workflows/ci.yml | 23 +++++++++++++++++------ src/main/resources/application-ci.yml | 10 ++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/application-ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89c1fe0..c69ca97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,16 @@ permissions: jobs: build: runs-on: ubuntu-latest - env: - JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root # Docker 내부 비밀번호 지정 + MYSQL_DATABASE: team8_test # Docker 내부 DB 이름 + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping --silent" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v4 @@ -22,7 +30,10 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Build and Test with Gradle - uses: gradle/actions/setup-gradle@v3 - with: - arguments: clean build \ No newline at end of file + - name: Wait for MySQL to be ready + run: sleep 15 + + - name: Run Test with Gradle + env: + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + run: ./gradlew clean test -Dspring.profiles.active=ci \ No newline at end of file diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml new file mode 100644 index 0000000..6fcaf65 --- /dev/null +++ b/src/main/resources/application-ci.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/team8_test + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + +jwt: + secret: + key: ${JWT_SECRET_KEY} \ No newline at end of file From f435aec4cb7e322de7bd1ec5f54a20cf0ba4053f Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Tue, 25 Mar 2025 16:46:08 +0900 Subject: [PATCH 020/164] =?UTF-8?q?feat(search):=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=9D=B8=EA=B8=B0=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 26 +++++++++++++++ .../domain/search/dto/PopularKeywordDto.java | 13 ++++++++ .../domain/search/entity/SearchLog.java | 30 +++++++++++++++++ .../controller/ProductController.java | 14 ++++++++ .../fakeProduct/service/ProductService.java | 12 ++++++- .../repository/SearchLogRepository.java | 20 +++++++++++ .../domain/search/service/SearchService.java | 33 +++++++++++++++++++ 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/eightyage/domain/search/controller/SearchController.java create mode 100644 src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java create mode 100644 src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java create mode 100644 src/main/java/com/example/eightyage/domain/search/repository/SearchLogRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/search/service/SearchService.java diff --git a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java new file mode 100644 index 0000000..1c31a3e --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java @@ -0,0 +1,26 @@ +package com.example.eightyage.domain.search.controller; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.service.SearchService; +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 java.util.List; + +@RestController +@RequiredArgsConstructor +public class SearchController { + + private final SearchService searchService; + + @GetMapping("/api/v1/search/popular") + public ResponseEntity> searchPopularKeywords( + @RequestParam(defaultValue = "7") int days + ) { + return ResponseEntity.ok(searchService.searchPoplarKeywordsWithinDays(days)); + } + +} diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java new file mode 100644 index 0000000..10d6cbe --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -0,0 +1,13 @@ +package com.example.eightyage.domain.search.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PopularKeywordDto { + + private String keyword; + private long count; + +} diff --git a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java new file mode 100644 index 0000000..1d88ca4 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java @@ -0,0 +1,30 @@ +package com.example.eightyage.domain.search.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor +@Getter +public class SearchLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String keyword; + private LocalDateTime searchedAt; + + public static SearchLog of(String keyword) { + SearchLog log = new SearchLog(); + log.keyword = keyword; + log.searchedAt = LocalDateTime.now(); + return log; + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java index 7725ede..458da19 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java @@ -27,4 +27,18 @@ public ResponseEntity> searchProduct( return ResponseEntity.ok(productService.getProducts(name, category, size, page)); } +// @GetMapping("/api/v1/products") +// public ResponseEntity> searchProduct( +// @RequestParam(required = false) String name, +// @RequestParam(required = false) Category category, +// @RequestParam(defaultValue = "10") int size, +// @RequestParam(defaultValue = "1") int page +// ) { +// Page results = productService.getProducts(name, category, size, page); +// if (StringUtils.hasText(name) && !results.isEmpty()){ +// searchService.saveKeyword(name); +// } +// return ResponseEntity.ok(results); +// } + } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java index f039ed8..3ba8ed3 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java @@ -4,24 +4,34 @@ import com.example.eightyage.domain.search.fakeProduct.entity.Category; import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; +import com.example.eightyage.domain.search.service.SearchService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; + private final SearchService searchService; @Transactional(readOnly = true) public Page getProducts(String productName, Category category, int size, int page) { - int adjustedPage = (page > 0) ? page - 1 : 0; + int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); Page products = productRepository.findProducts(productName, category, pageable); + + if (StringUtils.hasText(productName) && !products.isEmpty()) { + searchService.saveKeyword(productName); + } + return products.map(ProductSearchResponse::from); } + + } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/repository/SearchLogRepository.java b/src/main/java/com/example/eightyage/domain/search/repository/SearchLogRepository.java new file mode 100644 index 0000000..21c5bd1 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/repository/SearchLogRepository.java @@ -0,0 +1,20 @@ +package com.example.eightyage.domain.search.repository; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.entity.SearchLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface SearchLogRepository extends JpaRepository { + + @Query("SELECT new com.example.eightyage.domain.search.dto.PopularKeywordDto(s.keyword, COUNT(s))" + + "FROM SearchLog s " + + "WHERE s.searchedAt >= :since " + + "GROUP BY s.keyword " + + "ORDER BY COUNT(s) DESC ") + List findPopularKeywords(@Param("since") LocalDateTime since); +} diff --git a/src/main/java/com/example/eightyage/domain/search/service/SearchService.java b/src/main/java/com/example/eightyage/domain/search/service/SearchService.java new file mode 100644 index 0000000..ff5b772 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/SearchService.java @@ -0,0 +1,33 @@ +package com.example.eightyage.domain.search.service; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.entity.SearchLog; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import com.example.eightyage.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchService { + + private final SearchLogRepository searchLogRepository; + + public void saveKeyword(String keyword){ + if(StringUtils.hasText(keyword)){ + searchLogRepository.save(SearchLog.of(keyword)); + } + } + + public List searchPoplarKeywordsWithinDays(int days) { + if(days<0 || days >365){ + throw new BadRequestException("조회 기간은 1 ~ 365일 사이여야 합니다."); + } + LocalDateTime since = LocalDateTime.now().minusDays(days); + return searchLogRepository.findPopularKeywords(since); + } +} From 4d28fccc8aec33b82bc02e6fc87df865a0593ea6 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 11:51:25 +0900 Subject: [PATCH 021/164] =?UTF-8?q?feat(user):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EA=B5=AC=ED=98=84=20#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - soft delete를 사용해 deleteAt에 현재시간으로 업데이트 - 만약 deletedAt에 현재시간이 있으면 삭제된 정보임 - 비밀번호를 통해 회원탈퇴 가능 - 탈퇴한 이메일로 로그인 불가능 --- .../domain/auth/service/AuthService.java | 5 +++ .../user/controller/UserController.java | 33 +++++++++++++++++++ .../user/dto/request/UserDeleteRequest.java | 10 ++++++ .../eightyage/domain/user/entity/User.java | 19 +++++++++++ .../user/repository/UserRepository.java | 12 +++++-- .../domain/user/service/UserService.java | 17 ++++++++++ .../eightyage/global/entity/TimeStamped.java | 2 ++ 7 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/user/controller/UserController.java create mode 100644 src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index d69456b..0340b6c 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -21,6 +21,7 @@ public class AuthService { private final PasswordEncoder passwordEncoder; /* 회원가입 */ + @Transactional public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { @@ -37,6 +38,10 @@ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { public AuthTokensResponseDto signin(AuthSigninRequestDto request) { User user = userService.findUserByEmailOrElseThrow(request.getEmail()); + if (user.getDeletedAt() != null) { + throw new UnauthorizedException("탈퇴한 유저 이메일입니다."); + } + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new UnauthorizedException("잘못된 비밀번호입니다."); } diff --git a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java new file mode 100644 index 0000000..21fce31 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java @@ -0,0 +1,33 @@ +package com.example.eightyage.domain.user.controller; + +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.dto.AuthUser; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class UserController { + + private final UserService userService; + + /* 회원탈퇴 */ + @PostMapping("/v1/users/delete") + public void signup( + @AuthenticationPrincipal AuthUser authUser, + @RequestBody UserDeleteRequest request + ) { + userService.deleteUser(authUser, request); + } +} diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java new file mode 100644 index 0000000..06edae2 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java @@ -0,0 +1,10 @@ +package com.example.eightyage.domain.user.dto.request; + +import lombok.Getter; + +@Getter +public class UserDeleteRequest { + + private String password; + +} diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index bc109b4..2809145 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -1,10 +1,13 @@ package com.example.eightyage.domain.user.entity; +import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Getter @Entity @NoArgsConstructor @@ -30,4 +33,20 @@ public User(String email, String nickname, String password) { this.password = password; this.userRole = UserRole.ROLE_USER; } + + public User(Long id, String email, String nickname, UserRole userRole) { + this.id = id; + this.email = email; + this.nickname = nickname; + this.userRole = userRole; + } + + public static User fromAuthUser(AuthUser authUser) { + return new User(authUser.getUserId(), authUser.getEmail(), authUser.getEmail(), + UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())); + } + + public void deleteUser() { + setDeletedAt(LocalDateTime.now()); + } } diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java index 6ab9a06..fa2023e 100644 --- a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java @@ -2,11 +2,19 @@ import com.example.eightyage.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); - Optional findByEmail(String email); -} + Optional findByEmail(@Param("email") String email); + + @Query("SELECT u FROM User u " + + "WHERE u.id = :userId " + + "AND u.deletedAt IS NULL") + Optional findById(@Param("userId") Long id); +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index fec30fc..6b07a80 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -1,13 +1,16 @@ package com.example.eightyage.domain.user.service; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -16,6 +19,8 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + /* 훠원저장 */ + @Transactional public User saveUser(String email, String nickname, String password) { if (userRepository.existsByEmail(email)) { @@ -29,6 +34,18 @@ public User saveUser(String email, String nickname, String password) { return userRepository.save(user); } + /* 회원탈퇴 */ + @Transactional + public void deleteUser(AuthUser authUser, UserDeleteRequest request) { + User findUser = findUserByIdOrElseThrow(authUser.getUserId()); + + if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { + throw new BadRequestException("비밀번호가 일치하지 않습니다."); + } + + findUser.deleteUser(); + } + public User findUserByEmailOrElseThrow(String email) { return userRepository.findByEmail(email).orElseThrow( () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index 5b5e58d..9bec7c0 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -23,6 +24,7 @@ public abstract class TimeStamped { @Temporal(TemporalType.TIMESTAMP) private LocalDateTime modifiedAt; + @Setter @Column @Temporal(TemporalType.TIMESTAMP) private LocalDateTime deletedAt; From 9304d716d4e4a902c348a4d998b9d44100f52ba0 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 14:26:45 +0900 Subject: [PATCH 022/164] =?UTF-8?q?test(auth):=20AuthServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - 회원가입_비밀번호_확인_불일치_실패 - 회원가입_성공 - 로그인_삭제된_유저의_이메일일_경우_실패 - 로그인_비밀번호가_일치하지_않을_경우_실패 - 로그인_성공 - 토큰_재발급_성공 --- .../dto/request/AuthSigninRequestDto.java | 2 + .../dto/request/AuthSignupRequestDto.java | 2 + .../domain/auth/service/AuthService.java | 2 +- .../domain/auth/service/AuthServiceTest.java | 144 ++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java index 3b08a29..3246310 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java @@ -1,8 +1,10 @@ package com.example.eightyage.domain.auth.dto.request; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class AuthSigninRequestDto { private String email; diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index 0defb39..482e7c0 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -3,9 +3,11 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class AuthSignupRequestDto { @NotBlank(message = "이메일은 필수 입력 값입니다.") diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 0340b6c..a9100fa 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -25,7 +25,7 @@ public class AuthService { public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { - throw new BadRequestException("비밀번호 확인을 입력해주세요"); + throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); } User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java new file mode 100644 index 0000000..1faab8c --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -0,0 +1,144 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.dto.request.AuthSigninRequestDto; +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.UnauthorizedException; +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.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +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.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class AuthServiceTest { + + @Mock + private UserService userService; + @Mock + private TokenService tokenService; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private AuthService authService; + + @Test + void 회원가입_비밀번호_확인_불일치_실패() { + // given + AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341"); + + // when & then + assertThrows(BadRequestException.class, + () -> authService.signup(passwordCheckErrorSignupDto), + "비밀번호가 비밀번호 확인과 일치하지 않습니다."); + } + + @Test + void 회원가입_성공() { + // given + AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234"); + User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); + String accessToken = "accessToken"; + String refreshToken = "refreshToken"; + + given(userService.saveUser(any(String.class), any(String.class), any(String.class))).willReturn(user); + given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); + + // when + AuthTokensResponseDto result = authService.signup(successSignupDto); + + // then + assertEquals(accessToken, result.getAccessToken()); + assertEquals(refreshToken, result.getRefreshToken()); + } + + @Test + void 로그인_삭제된_유저의_이메일일_경우_실패() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", LocalDateTime.now()); + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + + // when & then + assertThrows(UnauthorizedException.class, + () -> authService.signin(seccessSigninDto), + "탈퇴한 유저 이메일입니다."); + } + + @Test + void 로그인_비밀번호가_일치하지_않을_경우_실패() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", null); + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(false); + + // when & then + assertThrows(UnauthorizedException.class, + () -> authService.signin(seccessSigninDto), + "잘못된 비밀번호입니다."); + } + + @Test + void 로그인_성공() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", null); + + String accessToken = "accessToken"; + String refreshToken = "refreshToken"; + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(true); + given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); + + // when + AuthTokensResponseDto result = authService.signin(seccessSigninDto); + + // then + assertEquals(accessToken, result.getAccessToken()); + assertEquals(refreshToken, result.getRefreshToken()); + } + + @Test + void 토큰_재발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refreshToken"; + + String reissuedAccessToken = "reissued-accessToken"; + String reissuedRefreshToken = "reissued-refreshToken"; + + given(tokenService.reissueToken(refreshToken)).willReturn(user); + given(tokenService.createAccessToken(any(User.class))).willReturn(reissuedAccessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(reissuedRefreshToken); + + // when + AuthTokensResponseDto result = authService.reissueAccessToken(refreshToken); + + // then + assertEquals(reissuedAccessToken, result.getAccessToken()); + assertEquals(reissuedRefreshToken, result.getRefreshToken()); + } +} From 77ca670e8b402f552c697d58fa73e5aa77fb0e50 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 14:43:26 +0900 Subject: [PATCH 023/164] =?UTF-8?q?test(auth):=20TokenServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - 토큰발급_AccessToken_발급_성공 - 토큰발급_RefreshToken_발급_성공 - 토큰유효성검사_성공 - 토큰유효성검사_비활성_상태일때_실패 - 토큰검색_토큰이_없을_시_실패 ### Test Coverage (line: 26%) --- .../domain/auth/service/TokenServiceTest.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java new file mode 100644 index 0000000..4ae6465 --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -0,0 +1,121 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import com.example.eightyage.domain.auth.repository.RefreshTokenRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.NotFoundException; +import com.example.eightyage.global.exception.UnauthorizedException; +import com.example.eightyage.global.util.JwtUtil; +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 java.util.Optional; + +import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TokenServiceTest { + + @Mock + private RefreshTokenRepository refreshTokenRepository; + @Mock + private UserService userService; + @Mock + private JwtUtil jwtUtil; + + @InjectMocks + private TokenService tokenService; + + /* createAccessToken */ + @Test + void 토큰발급_AccessToken_발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String accessToken = "accessToken"; + + given(jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole())).willReturn(accessToken); + + // when + String result = tokenService.createAccessToken(user); + + // then + assertEquals(accessToken, result); + } + + /* createRefreshToken */ + @Test + void 토큰발급_RefreshToken_발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + RefreshToken mockRefreshToken = new RefreshToken(user.getId()); + + given(refreshTokenRepository.save(any(RefreshToken.class))).willReturn(mockRefreshToken); + + // when + String createdRefreshToken = tokenService.createRefreshToken(user); + + // then + verify(refreshTokenRepository, times(1)).save(any(RefreshToken.class)); + assertEquals(mockRefreshToken.getToken(), createdRefreshToken); + } + + /* reissueToken */ + @Test + void 토큰유효성검사_비활성_상태일때_실패() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refresh-token"; + + RefreshToken mockRefreshToken = mock(RefreshToken.class); + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.of(mockRefreshToken)); + given(mockRefreshToken.getTokenState()).willReturn(INVALIDATED); + + // when & then + assertThrows(UnauthorizedException.class, + () -> tokenService.reissueToken(refreshToken), + "사용이 만료된 refresh token 입니다."); + } + + @Test + void 토큰검색_토큰이_없을_시_실패() { + //given + String refreshToken = "refresh-token"; + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> tokenService.reissueToken(refreshToken), + "리프레시 토큰을 찾을 수 없습니다."); + } + + @Test + void 토큰유효성검사_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refresh-token"; + + RefreshToken mockRefreshToken = mock(RefreshToken.class); + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.of(mockRefreshToken)); + given(userService.findUserByIdOrElseThrow(mockRefreshToken.getUserId())).willReturn(user); + + // when + User result = tokenService.reissueToken(refreshToken); + + // then + assertNotNull(result); + verify(mockRefreshToken, times(1)).updateTokenStatus(INVALIDATED); + } +} From 3a58b13d27b5eed8ff45322e61e724d287b5f053 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:10:33 +0900 Subject: [PATCH 024/164] =?UTF-8?q?test(user):=20UserServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - findById조회_userId가_없을_경우_실패 - findById조회_성공 - findByEmail조회_email이_없을_경우_실패 - findByEmail조회_성공 - 회원저장_중복된_이메일이_있을_경우_실패 - 회원저장_성공 - 회원탈퇴_회원이_존재하지_않으면_실패 - 회원탈퇴_비밀번호가_일치하지_않으면_실패 - 회원탈퇴_성공 ### Test Coverage (line: 38%) --- .../user/dto/request/UserDeleteRequest.java | 2 + .../domain/user/service/UserService.java | 2 +- .../domain/user/service/UserServiceTest.java | 197 ++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java index 06edae2..ef250c7 100644 --- a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java @@ -1,8 +1,10 @@ package com.example.eightyage.domain.user.dto.request; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class UserDeleteRequest { private String password; diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 6b07a80..0c86fb1 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -40,7 +40,7 @@ public void deleteUser(AuthUser authUser, UserDeleteRequest request) { User findUser = findUserByIdOrElseThrow(authUser.getUserId()); if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { - throw new BadRequestException("비밀번호가 일치하지 않습니다."); + throw new UnauthorizedException("비밀번호가 일치하지 않습니다."); } findUser.deleteUser(); diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..7e03f5f --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -0,0 +1,197 @@ +package com.example.eightyage.domain.user.service; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import com.example.eightyage.domain.auth.service.TokenService; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.NotFoundException; +import com.example.eightyage.global.exception.UnauthorizedException; +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.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserService userService; + + /* findUserByIdOrElseThrow */ + @Test + void findById조회_userId가_없을_경우_실패() { + // given + Long userId = 1L; + + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> userService.findUserByIdOrElseThrow(userId), + "해당 유저의 Id를 찾을 수 없습니다."); + } + + @Test + void findById조회_성공() { + // given + Long userId = 1L; + User user = new User(userId, "email@email.com", "nickname", UserRole.ROLE_USER); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + + // when + User resultUser = userService.findUserByIdOrElseThrow(userId); + + // then + assertNotNull(resultUser); + assertEquals(user.getId(), resultUser.getId()); + assertEquals(user.getEmail(), resultUser.getEmail()); + assertEquals(user.getNickname(), resultUser.getNickname()); + assertEquals(user.getUserRole(), resultUser.getUserRole()); + } + + /* findUserByEmailOrElseThrow */ + @Test + void findByEmail조회_email이_없을_경우_실패() { + // given + String email = "email@email.com"; + + given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty()); + + // when & then + assertThrows(UnauthorizedException.class, + () -> userService.findUserByEmailOrElseThrow(email), + "가입한 유저의 이메일이 아닙니다."); + } + + @Test + void findByEmail조회_성공() { + // given + String email = "email@email.com"; + User user = new User(1L, email, "nickname", UserRole.ROLE_USER); + + given(userRepository.findByEmail(any(String.class))).willReturn(Optional.of(user)); + + // when + User resultUser = userService.findUserByEmailOrElseThrow(email); + + // then + assertNotNull(resultUser); + assertEquals(user.getId(), resultUser.getId()); + assertEquals(user.getEmail(), resultUser.getEmail()); + assertEquals(user.getNickname(), resultUser.getNickname()); + assertEquals(user.getUserRole(), resultUser.getUserRole()); + } + + /* saveUser */ + @Test + void 회원저장_중복된_이메일이_있을_경우_실패() { + // given + String email = "email@email.com"; + String nickname = "nickname"; + String password = "password1234"; + + given(userRepository.existsByEmail(any(String.class))).willReturn(true); + + // when & then + assertThrows(BadRequestException.class, + () -> userService.saveUser(email, nickname, password), + "등록된 이메일입니다."); + } + + @Test + void 회원저장_성공() { + // given + String email = "email@email.com"; + String nickname = "nickname"; + String password = "password1234"; + User user = new User(email, nickname, password); + + String encodedPassword = "encoded-password1234"; + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(passwordEncoder.encode(any(String.class))).willReturn(encodedPassword); + given(userRepository.save(any(User.class))).willReturn(user); + + // when + User resultUser = userService.saveUser(email, nickname, password); + + // then + assertNotNull(resultUser); + assertEquals(email, resultUser.getEmail()); + assertEquals(nickname, resultUser.getNickname()); + assertEquals(password, resultUser.getPassword()); + } + + /* deleteUser */ + @Test + void 회원탈퇴_회원이_존재하지_않으면_실패() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); + + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> userService.deleteUser(authUser, successDeleteDto), + "해당 유저의 Id를 찾을 수 없습니다."); + } + + @Test + void 회원탈퇴_비밀번호가_일치하지_않으면_실패() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "password", "password1234"); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(false); + + // when & then + assertThrows(UnauthorizedException.class, + () -> userService.deleteUser(authUser, successDeleteDto), + "비밀번호가 일치하지 않습니다."); + } + + @Test + void 회원탈퇴_성공() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234"); + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "password", "password1234"); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(true); + + // when + userService.deleteUser(authUser, successDeleteDto); + + // then + assertNotNull(user.getDeletedAt()); + + } +} From 9df94437918452fe297dec8ba355f262e1753b91 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:25:32 +0900 Subject: [PATCH 025/164] =?UTF-8?q?fix(global):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 에러코드를 출력할 수 있도록 @ResponseStatus 및 ResponseEntity 사용 --- .../global/exception/GlobalExceptionHandler.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index d297f34..79194a0 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -3,9 +3,11 @@ import com.example.eightyage.global.entity.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; @@ -15,11 +17,12 @@ public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) - public ErrorResponse invalidRequestExceptionException(CustomException ex) { + public ResponseEntity> invalidRequestExceptionException(CustomException ex) { HttpStatus httpStatus = ex.getHttpStatus(); - return ErrorResponse.of(httpStatus, ex.getMessage()); + return new ResponseEntity<>(ErrorResponse.of(httpStatus, ex.getMessage()), ex.getHttpStatus()); } + @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler public ErrorResponse> handleValidationException(MethodArgumentNotValidException e) { List fieldErrors = e.getBindingResult().getFieldErrors(); @@ -30,6 +33,7 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa return ErrorResponse.of(HttpStatus.BAD_REQUEST, validFailedList); } + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); From f47878c6df5eb44da055485fccfb490b8d9945a4 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:37:44 +0900 Subject: [PATCH 026/164] =?UTF-8?q?fix(auth):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20ADMIN=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 관리자 테스트도 용이하게 만들기 위해 ADMIN을 작성하여 회원가입을 할수 있도록 수정 --- .../domain/auth/dto/request/AuthSignupRequestDto.java | 2 ++ .../eightyage/domain/auth/service/AuthService.java | 2 +- .../com/example/eightyage/domain/user/entity/User.java | 4 ++-- .../eightyage/domain/user/service/UserService.java | 5 +++-- .../eightyage/domain/auth/service/AuthServiceTest.java | 6 +++--- .../eightyage/domain/user/service/UserServiceTest.java | 8 +++++--- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index 482e7c0..c1687bf 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -23,4 +23,6 @@ public class AuthSignupRequestDto { private String password; private String passwordCheck; + + private String userRole; } diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index a9100fa..15c3a86 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -28,7 +28,7 @@ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); } - User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); + User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword(), request.getUserRole()); return getTokenResponse(user); } diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index 2809145..cfaa42f 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -27,11 +27,11 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String nickname, String password) { + public User(String email, String nickname, String password, UserRole userRole) { this.email = email; this.nickname = nickname; this.password = password; - this.userRole = UserRole.ROLE_USER; + this.userRole = userRole; } public User(Long id, String email, String nickname, UserRole userRole) { diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 0c86fb1..cc9364f 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -2,6 +2,7 @@ import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; @@ -21,7 +22,7 @@ public class UserService { /* 훠원저장 */ @Transactional - public User saveUser(String email, String nickname, String password) { + public User saveUser(String email, String nickname, String password, String userRole) { if (userRepository.existsByEmail(email)) { throw new BadRequestException("등록된 이메일입니다."); @@ -29,7 +30,7 @@ public User saveUser(String email, String nickname, String password) { String encodedPassword = passwordEncoder.encode(password); - User user = new User(email, nickname, encodedPassword); + User user = new User(email, nickname, encodedPassword, UserRole.of(userRole)); return userRepository.save(user); } diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java index 1faab8c..6843198 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -39,7 +39,7 @@ public class AuthServiceTest { @Test void 회원가입_비밀번호_확인_불일치_실패() { // given - AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341"); + AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341", "USER_ROLE"); // when & then assertThrows(BadRequestException.class, @@ -50,12 +50,12 @@ public class AuthServiceTest { @Test void 회원가입_성공() { // given - AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234"); + AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234", "USER_ROLE"); User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); String accessToken = "accessToken"; String refreshToken = "refreshToken"; - given(userService.saveUser(any(String.class), any(String.class), any(String.class))).willReturn(user); + given(userService.saveUser(any(String.class), any(String.class), any(String.class), any(String.class))).willReturn(user); given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java index 7e03f5f..0fa1b70 100644 --- a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -111,12 +111,13 @@ public class UserServiceTest { String email = "email@email.com"; String nickname = "nickname"; String password = "password1234"; + String userRole = "USER_ROLE"; given(userRepository.existsByEmail(any(String.class))).willReturn(true); // when & then assertThrows(BadRequestException.class, - () -> userService.saveUser(email, nickname, password), + () -> userService.saveUser(email, nickname, password, userRole), "등록된 이메일입니다."); } @@ -126,7 +127,8 @@ public class UserServiceTest { String email = "email@email.com"; String nickname = "nickname"; String password = "password1234"; - User user = new User(email, nickname, password); + String userRole = "ROLE_USER"; + User user = new User(email, nickname, password, UserRole.ROLE_USER); String encodedPassword = "encoded-password1234"; @@ -135,7 +137,7 @@ public class UserServiceTest { given(userRepository.save(any(User.class))).willReturn(user); // when - User resultUser = userService.saveUser(email, nickname, password); + User resultUser = userService.saveUser(email, nickname, password, userRole); // then assertNotNull(resultUser); From 520ec77cf65551d75469236752d06c8185186a88 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 17:35:30 +0900 Subject: [PATCH 027/164] =?UTF-8?q?fix(ci)=20:=20CI=EC=9A=A9=20application?= =?UTF-8?q?-ci.yml=20=EC=B6=94=EA=B0=80=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 추가 - Docker MySQL 사용하도록 수정 --- .github/workflows/ci.yml | 13 ++++++++++++- src/main/resources/application-ci.yml | 15 +++++++++++++-- .../eightyage/EightyageApplicationTests.java | 13 ------------- 3 files changed, 25 insertions(+), 16 deletions(-) delete mode 100644 src/test/java/com/example/eightyage/EightyageApplicationTests.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c69ca97..3aab739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,18 @@ jobs: distribution: 'temurin' - name: Wait for MySQL to be ready - run: sleep 15 + run: | + for i in {1..10}; do + if mysql -hmysql -P3306 -uroot -proot -e "SELECT 1"; then + echo "MySQL is up!" + break + fi + echo "Waiting for MySQL..." + sleep 5 + done + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew - name: Run Test with Gradle env: diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 6fcaf65..76a6120 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -1,10 +1,21 @@ +server: + port: 8080 + spring: datasource: - url: jdbc:mysql://localhost:3306/team8_test + url: jdbc:mysql://mysql:3306/team8_test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + show_sql: true + format_sql: true jwt: secret: - key: ${JWT_SECRET_KEY} \ No newline at end of file + key: vA1z3L5pQ8sK0yHdWfJdXhMTJpS9gA2fByKr6B+3UkE= \ No newline at end of file diff --git a/src/test/java/com/example/eightyage/EightyageApplicationTests.java b/src/test/java/com/example/eightyage/EightyageApplicationTests.java deleted file mode 100644 index d2e270e..0000000 --- a/src/test/java/com/example/eightyage/EightyageApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.eightyage; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class EightyageApplicationTests { - - @Test - void contextLoads() { - } - -} From ef4909222c0d3ba27cc2d4c505e9ddfcc0dc87fa Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 17:40:17 +0900 Subject: [PATCH 028/164] =?UTF-8?q?refactor(ci)=20:=20application-ci.yml?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 수정 --- src/main/resources/application-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 76a6120..fb7a102 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -18,4 +18,4 @@ spring: format_sql: true jwt: secret: - key: vA1z3L5pQ8sK0yHdWfJdXhMTJpS9gA2fByKr6B+3UkE= \ No newline at end of file + key: ${JWT_SECRET_KEY} \ No newline at end of file From 5b6a4292e07fae63a29793c3944de869e8833b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 18:57:14 +0900 Subject: [PATCH 029/164] =?UTF-8?q?feat:=20=EC=A0=9C=ED=92=88=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EC=A0=9C=ED=92=88=20=EB=8B=A8=EA=B1=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=EC=A0=9C=ED=92=88=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EC=A0=9C=ED=92=88=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 59 ++++++++++++++++ .../dto/request/ProductSaveRequestDto.java | 25 +++++++ .../dto/request/ProductUpdateRequestDto.java | 20 ++++++ .../dto/response/ProductGetResponseDto.java | 27 ++++++++ .../dto/response/ProductSaveResponseDto.java | 25 +++++++ .../response/ProductUpdateResponseDto.java | 27 ++++++++ .../domain/product/entity/Category.java | 11 +-- .../domain/product/entity/Product.java | 21 ++++-- .../product/repository/ProductRepository.java | 15 ++++ .../product/service/ProductService.java | 69 +++++++++++++++++++ .../eightyage/global/entity/TimeStamped.java | 3 +- 11 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/product/controller/ProductController.java create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/product/service/ProductService.java diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java new file mode 100644 index 0000000..28df14a --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -0,0 +1,59 @@ +package com.example.eightyage.domain.product.controller; + +import com.example.eightyage.domain.product.dto.request.ProductSaveRequestDto; +import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; +import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; +import com.example.eightyage.domain.product.service.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/products") +@RequiredArgsConstructor +public class ProductController { + + private final ProductService productService; + + // 제품 생성 + @Secured("ROLE_ADMIN") + @PostMapping + public ResponseEntity saveProduct(@RequestBody ProductSaveRequestDto requestDto){ + productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + // 제품 수정 + @Secured("ROLE_ADMIN") + @PatchMapping("/{productId}") + public ResponseEntity updateProduct( + @PathVariable Long productId, + @RequestBody ProductUpdateRequestDto requestDto + ){ + ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getSaleState(), requestDto.getPrice()); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + // 제품 단건 조회 + @GetMapping("/{productId}") + public ResponseEntity getProduct(@PathVariable Long productId){ + ProductGetResponseDto responseDto = productService.findProductById(productId); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + // 제품 삭제 + @Secured("ROLE_ADMIN") + @DeleteMapping("/{productId}") + public ResponseEntity deleteProduct(@PathVariable Long productId){ + productService.deleteProduct(productId); + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java new file mode 100644 index 0000000..bcb56f1 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java @@ -0,0 +1,25 @@ +package com.example.eightyage.domain.product.dto.request; + +import com.example.eightyage.domain.product.entity.Category; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; + +@Getter +public class ProductSaveRequestDto { + + @NotBlank(message="반드시 값이 있어야 합니다.") + private String productName; + + @NotBlank(message="반드시 값이 있어야 합니다.") + private Category category; + + @NotBlank(message="반드시 값이 있어야 합니다.") + private String content; + + @NotNull(message="반드시 값이 있어야 합니다.") + private Integer price; +} diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java new file mode 100644 index 0000000..39e8e19 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -0,0 +1,20 @@ +package com.example.eightyage.domain.product.dto.request; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.SaleState; +import lombok.Getter; + +@Getter +public class ProductUpdateRequestDto { + + + private String productName; + + private Category category; + + private String content; + + private SaleState saleState; + + private Integer price; +} diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java new file mode 100644 index 0000000..62b8b26 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.product.dto.response; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.SaleState; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ProductGetResponseDto { + + private final String productName; + + private final String content; + + private final Category category; + + private final Integer price; + + private final SaleState saleState; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java new file mode 100644 index 0000000..f526d92 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java @@ -0,0 +1,25 @@ +package com.example.eightyage.domain.product.dto.response; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.SaleState; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ProductSaveResponseDto { + + private final String productName; + + private final Category category; + + private final Integer price; + + private final SaleState saleState; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java new file mode 100644 index 0000000..50723a6 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.product.dto.response; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.SaleState; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ProductUpdateResponseDto { + + private final String productName; + + private final Integer price; + + private final String content; + + private final Category category; + + private final SaleState saleState; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Category.java b/src/main/java/com/example/eightyage/domain/product/entity/Category.java index 15c887f..044799d 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Category.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Category.java @@ -10,16 +10,7 @@ public enum Category { CLEANSING("클렌징"), MASK_PACK("마스크팩"), MEN_CARE("남성용"), - TOOL("뷰티 도구"), - - // 피부 타입 - DRY_SKIN("건성"), - OILY_SKIN("지성"), - NORMAL_SKIN("중성"), - COMBINATION_SKIN("복합성"), - SENSITIVE_SKIN("민감성"), - ACNE_PRONE_SKIN("여드름성"), - ATOPIC_SKIN("아토피"); + TOOL("뷰티 도구"); private final String displayName; diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index d692eeb..b666769 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -4,8 +4,12 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; @Entity @Getter @@ -17,15 +21,24 @@ public class Product extends TimeStamped { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Setter private String name; @Enumerated(EnumType.STRING) - private Category category; + @Setter private Category category; - private String content; + @Setter private String content; - private Integer price; + @Setter private Integer price; @Enumerated(EnumType.STRING) - private SaleState saleState; + @Setter private SaleState saleState; + + public Product(String name, Category category, String content, Integer price, SaleState saleState) { + this.name = name; + this.category = category; + this.content = content; + this.price = price; + this.saleState = saleState; + } } diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java new file mode 100644 index 0000000..6ddba40 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -0,0 +1,15 @@ +package com.example.eightyage.domain.product.repository; + +import com.example.eightyage.domain.product.entity.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ProductRepository extends JpaRepository { + + @Query("SELECT p FROM Product p WHERE p.deletedAt IS NULL") + Optional findById(Long productId); +} diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java new file mode 100644 index 0000000..fa6f702 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -0,0 +1,69 @@ +package com.example.eightyage.domain.product.service; + +import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.global.exception.NotFoundException; +import com.example.eightyage.global.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + // 제품 생성 + @Transactional + public void saveProduct(String productName, Category category, String content, Integer price) { + Product product = new Product(productName, category, content, price, SaleState.FOR_SALE); + + productRepository.save(product); + } + + // 제품 수정 + @Transactional + public ProductUpdateResponseDto updateProduct(Long productId, String productName, Category category, String content, SaleState saleState, Integer price) { + Product findProduct = productRepository.findById(productId).orElseThrow( + () -> new NotFoundException("존재하지 않는 상품입니다.") + ); + + if(productName != null) findProduct.setName(productName); + if(category != null) findProduct.setCategory(category); + if(content != null) findProduct.setContent(content); + if(saleState != null) findProduct.setSaleState(saleState); + if(price != null) findProduct.setPrice(price); + + return new ProductUpdateResponseDto(findProduct.getName(), findProduct.getPrice(), findProduct.getContent(), findProduct.getCategory(), findProduct.getSaleState(), findProduct.getCreatedAt(), findProduct.getModifiedAt()); + } + + // 제품 단건 조회 + @Transactional(readOnly = true) + public ProductGetResponseDto findProductById(Long productId) { + Product findProduct = productRepository.findById(productId).orElseThrow( + () -> new NotFoundException("존재하지 않는 상품입니다.") + ); + + return new ProductGetResponseDto(findProduct.getName(), findProduct.getContent(), findProduct.getCategory(), findProduct.getPrice(), findProduct.getSaleState(), findProduct.getCreatedAt(), findProduct.getModifiedAt()); + } + + // 제품 삭제 + @Transactional + public void deleteProduct(Long productId) { + Product findProduct = productRepository.findById(productId).orElseThrow( + () -> new NotFoundException("존재하지 않는 상품입니다.") + ); + + findProduct.setDeletedAt(LocalDateTime.now()); + } +} diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index 5b5e58d..c487799 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -25,5 +26,5 @@ public abstract class TimeStamped { @Column @Temporal(TemporalType.TIMESTAMP) - private LocalDateTime deletedAt; + @Setter private LocalDateTime deletedAt; } From 683f13e1461b24662a993b1d7554d96282b8bfb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 18:58:27 +0900 Subject: [PATCH 030/164] =?UTF-8?q?chore:=20.gitkeep=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/eightyage/domain/product/controller/.gitkeep | 0 .../com/example/eightyage/domain/product/dto/request/.gitkeep | 0 .../com/example/eightyage/domain/product/dto/response/.gitkeep | 0 .../java/com/example/eightyage/domain/product/entity/.gitkeep | 0 .../java/com/example/eightyage/domain/product/repository/.gitkeep | 0 .../java/com/example/eightyage/domain/product/service/.gitkeep | 0 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/product/controller/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/product/dto/request/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/product/dto/response/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/product/entity/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/product/repository/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/product/service/.gitkeep diff --git a/src/main/java/com/example/eightyage/domain/product/controller/.gitkeep b/src/main/java/com/example/eightyage/domain/product/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/.gitkeep b/src/main/java/com/example/eightyage/domain/product/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/.gitkeep b/src/main/java/com/example/eightyage/domain/product/dto/response/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/product/entity/.gitkeep b/src/main/java/com/example/eightyage/domain/product/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/product/repository/.gitkeep b/src/main/java/com/example/eightyage/domain/product/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/product/service/.gitkeep b/src/main/java/com/example/eightyage/domain/product/service/.gitkeep deleted file mode 100644 index e69de29..0000000 From 95a144fae0ddf85e875d75e1a48744d78340712b Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 19:37:59 +0900 Subject: [PATCH 031/164] =?UTF-8?q?refactor(ci)=20:=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EC=A7=84=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - ./gradlew clean test에서 ./gradlew clean build로 수정 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aab739..19cb1c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: services: mysql: - image: mysql:8.0 + image: mysql:8.0 # MySQL 컨테이너 띄우기 env: MYSQL_ROOT_PASSWORD: root # Docker 내부 비밀번호 지정 MYSQL_DATABASE: team8_test # Docker 내부 DB 이름 @@ -44,7 +44,7 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: Run Test with Gradle + - name: Test And Build with Gradle env: JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - run: ./gradlew clean test -Dspring.profiles.active=ci \ No newline at end of file + run: ./gradlew clean build -Dspring.profiles.active=ci \ No newline at end of file From fa2b1bcbbb1868ecee9a37e87d0cb780b92b61bf Mon Sep 17 00:00:00 2001 From: peridot Date: Tue, 25 Mar 2025 19:36:07 +0900 Subject: [PATCH 032/164] =?UTF-8?q?feat(event):=20admin=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=97=86=EC=9D=B4=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(event): admin 권한 없이 CRUD --- .../eightyage/domain/coupon/entity/.gitkeep | 0 .../domain/coupon/entity/Coupon.java | 29 +++++++ .../domain/coupon/entity/CouponState.java | 6 ++ .../event/controller/EventController.java | 37 +++++++++ .../event/dto/request/EventRequestDto.java | 29 +++++++ .../event/dto/response/EventResponseDto.java | 27 +++++++ .../eightyage/domain/event/entity/Event.java | 63 +++++++++++++++ .../domain/event/entity/EventState.java | 6 ++ .../event/repository/EventRepository.java | 9 +++ .../domain/event/service/EventService.java | 80 +++++++++++++++++++ 10 files changed, 286 insertions(+) delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java create mode 100644 src/main/java/com/example/eightyage/domain/event/controller/EventController.java create mode 100644 src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java create mode 100644 src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/event/entity/Event.java create mode 100644 src/main/java/com/example/eightyage/domain/event/entity/EventState.java create mode 100644 src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/event/service/EventService.java diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java new file mode 100644 index 0000000..2b488f1 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.coupon.entity; + +import com.example.eightyage.domain.event.entity.Event; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Coupon extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String coupon_number; + + @Enumerated(EnumType.STRING) + private CouponState state; + + @ManyToOne + private User user; + + @ManyToOne + private Event event; +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java new file mode 100644 index 0000000..057bf3f --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.coupon.entity; + +public enum CouponState { + VALID, + INVALIDATED +} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java new file mode 100644 index 0000000..39bc520 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -0,0 +1,37 @@ +package com.example.eightyage.domain.event.controller; + +import com.example.eightyage.domain.event.dto.request.EventRequestDto; +import com.example.eightyage.domain.event.dto.response.EventResponseDto; +import com.example.eightyage.domain.event.service.EventService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/event") +@RequiredArgsConstructor +public class EventController { + + private final EventService eventService; + + @PostMapping + public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { + return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); + } + + @GetMapping + public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(eventService.getEvents(page, size)); + } + + @GetMapping("/{eventId}") + public ResponseEntity getEvent(@PathVariable long eventId) { + return ResponseEntity.ok(eventService.getEvent(eventId)); + } + + @PatchMapping("/{eventId}") + public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { + return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java b/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java new file mode 100644 index 0000000..c0a6ce4 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.event.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class EventRequestDto { + + @NotBlank + private String name; + + @NotBlank + private String description; + + @NotBlank + private int quantity; + + @NotBlank + private LocalDateTime startDate; + + @NotBlank + private LocalDateTime endDate; +} diff --git a/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java b/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java new file mode 100644 index 0000000..98edeb2 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.event.dto.response; + +import com.example.eightyage.domain.event.entity.EventState; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class EventResponseDto { + + private final String name; + private final String description; + private final int quantity; + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private final EventState state; + + + public EventResponseDto(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate, EventState state) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; + this.state = state; + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/Event.java b/src/main/java/com/example/eightyage/domain/event/entity/Event.java new file mode 100644 index 0000000..4f71301 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/entity/Event.java @@ -0,0 +1,63 @@ +package com.example.eightyage.domain.event.entity; + +import com.example.eightyage.domain.event.dto.request.EventRequestDto; +import com.example.eightyage.domain.event.dto.response.EventResponseDto; +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor +public class Event extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String description; + private int quantity; + + @Column(name="start_at") + private LocalDateTime startDate; + @Column(name = "end_at") + private LocalDateTime endDate; + + @Enumerated(EnumType.STRING) + private EventState state; + + public Event(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; + } + + public void setState(EventState state) { + this.state = state; + } + + public EventResponseDto toDto() { + return new EventResponseDto( + this.getName(), + this.getDescription(), + this.getQuantity(), + this.getStartDate(), + this.getEndDate(), + this.getState() + ); + } + + public void update(EventRequestDto eventRequestDto) { + this.name = eventRequestDto.getName(); + this.description = eventRequestDto.getDescription(); + this.quantity = eventRequestDto.getQuantity(); + this.startDate = eventRequestDto.getStartDate(); + this.endDate = eventRequestDto.getEndDate(); + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/EventState.java b/src/main/java/com/example/eightyage/domain/event/entity/EventState.java new file mode 100644 index 0000000..75bb82f --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/entity/EventState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.event.entity; + +public enum EventState { + VALID, + INVALID +} diff --git a/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java b/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java new file mode 100644 index 0000000..f6ee989 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java @@ -0,0 +1,9 @@ +package com.example.eightyage.domain.event.repository; + +import com.example.eightyage.domain.event.entity.Event; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EventRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java new file mode 100644 index 0000000..72d637a --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -0,0 +1,80 @@ +package com.example.eightyage.domain.event.service; + +import com.example.eightyage.domain.event.dto.request.EventRequestDto; +import com.example.eightyage.domain.event.dto.response.EventResponseDto; +import com.example.eightyage.domain.event.entity.Event; +import com.example.eightyage.domain.event.entity.EventState; +import com.example.eightyage.domain.event.repository.EventRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class EventService { + + private final EventRepository eventRepository; + + public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { + Event event = new Event( + eventRequestDto.getName(), + eventRequestDto.getDescription(), + eventRequestDto.getQuantity(), + eventRequestDto.getStartDate(), + eventRequestDto.getEndDate() + ); + + checkEventState(event); + + Event savedEvent = eventRepository.save(event); + return savedEvent.toDto(); + } + + public Page getEvents(int page, int size) { + Pageable pageable = PageRequest.of(page, size); + Page events = eventRepository.findAll(pageable); + + // 모든 events들 checkState로 state 상태 갱신하기 + events.forEach(this::checkEventState); + + return events.map(Event::toDto); + } + + public EventResponseDto getEvent(long eventId) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new IllegalArgumentException("Event not found")); + + checkEventState(event); + + return event.toDto(); + } + + public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new IllegalArgumentException("Event not found")); + + event.update(eventRequestDto); + + checkEventState(event); + + return event.toDto(); + } + + private void checkEventState(Event event) { + LocalDateTime now = LocalDateTime.now(); + EventState newState = + ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && + (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) + ? EventState.VALID + : EventState.INVALID; + + if (event.getState() != newState) { + event.setState(newState); + eventRepository.save(event); + } + } +} From 67c7297f62c78bd46bb90ef44149bfa2150ce8df Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:49:50 +0900 Subject: [PATCH 033/164] =?UTF-8?q?refactor(auth):=20Builder=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 사용 --- .../dto/request/AuthSigninRequestDto.java | 2 ++ .../dto/request/AuthSignupRequestDto.java | 2 ++ .../response/AuthAccessTokenResponseDto.java | 2 ++ .../dto/response/AuthTokensResponseDto.java | 4 +++- .../domain/auth/entity/RefreshToken.java | 5 +++- ...Request.java => UserDeleteRequestDto.java} | 6 +++-- .../eightyage/domain/user/entity/User.java | 23 ++++++++++--------- .../eightyage/global/dto/AuthUser.java | 4 ++-- 8 files changed, 31 insertions(+), 17 deletions(-) rename src/main/java/com/example/eightyage/domain/user/dto/request/{UserDeleteRequest.java => UserDeleteRequestDto.java} (71%) diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java index 3246310..cfde537 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java @@ -1,10 +1,12 @@ package com.example.eightyage.domain.auth.dto.request; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter @AllArgsConstructor +@Builder public class AuthSigninRequestDto { private String email; diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index c1687bf..ad3680c 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -4,9 +4,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthSignupRequestDto { diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java index 660a03d..bb1890d 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java @@ -1,9 +1,11 @@ package com.example.eightyage.domain.auth.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthAccessTokenResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java index a7a77b3..6153686 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java @@ -1,13 +1,15 @@ package com.example.eightyage.domain.auth.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthTokensResponseDto { - private final String AccessToken; + private final String accessToken; private final String refreshToken; } diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java index 0c1c3c1..0c98161 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java +++ b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java @@ -1,6 +1,8 @@ package com.example.eightyage.domain.auth.entity; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,7 +10,7 @@ @Entity @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class RefreshToken { @Id @@ -22,6 +24,7 @@ public class RefreshToken { @Enumerated(EnumType.STRING) private TokenState tokenState; + @Builder public RefreshToken(Long userId) { this.userId = userId; this.token = UUID.randomUUID().toString(); diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java similarity index 71% rename from src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java rename to src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java index ef250c7..3054ccd 100644 --- a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java @@ -1,12 +1,14 @@ package com.example.eightyage.domain.user.dto.request; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor -public class UserDeleteRequest { +public class UserDeleteRequestDto { private String password; -} +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index cfaa42f..f208901 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -3,6 +3,8 @@ import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,7 +12,7 @@ @Getter @Entity -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends TimeStamped { @Id @@ -27,23 +29,22 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String nickname, String password, UserRole userRole) { - this.email = email; - this.nickname = nickname; - this.password = password; - this.userRole = userRole; - } - - public User(Long id, String email, String nickname, UserRole userRole) { + @Builder + public User(Long id, String email, String nickname, String password, UserRole userRole) { this.id = id; this.email = email; this.nickname = nickname; + this.password = password; this.userRole = userRole; } public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getUserId(), authUser.getEmail(), authUser.getEmail(), - UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())); + return User.builder() + .id(authUser.getUserId()) + .email(authUser.getEmail()) + .nickname(authUser.getNickname()) + .userRole(UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())) + .build(); } public void deleteUser() { diff --git a/src/main/java/com/example/eightyage/global/dto/AuthUser.java b/src/main/java/com/example/eightyage/global/dto/AuthUser.java index 8785cfe..022a0d4 100644 --- a/src/main/java/com/example/eightyage/global/dto/AuthUser.java +++ b/src/main/java/com/example/eightyage/global/dto/AuthUser.java @@ -1,7 +1,7 @@ package com.example.eightyage.global.dto; import com.example.eightyage.domain.user.entity.UserRole; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -10,7 +10,6 @@ import java.util.List; @Getter -@AllArgsConstructor public class AuthUser { private final Long userId; @@ -18,6 +17,7 @@ public class AuthUser { private final String nickname; private final Collection authorities; + @Builder public AuthUser(Long userId, String email, String nickname, UserRole role) { this.userId = userId; this.email = email; From 4626a20402b11ad3b937a1a5112b3fb3d4510411 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:52:59 +0900 Subject: [PATCH 034/164] =?UTF-8?q?refactor(auth):=20Builder=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20enum=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage가 관리할 수 있도록 enum 자료형 작성 --- .../domain/auth/service/AuthService.java | 13 ++++--- .../domain/auth/service/TokenService.java | 6 ++-- .../user/controller/UserController.java | 9 ++--- .../domain/user/entity/UserRole.java | 4 ++- .../domain/user/service/UserService.java | 21 ++++++++---- .../argument/RefreshArgumentResolver.java | 7 ++-- .../eightyage/global/exception/ErrorCode.java | 10 +++--- .../global/exception/ErrorMessage.java | 34 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 4 ++- .../filter/JwtAuthenticationFilter.java | 10 +++--- .../example/eightyage/global/util/.gitkeep | 0 .../eightyage/global/util/JwtUtil.java | 4 ++- 12 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/exception/ErrorMessage.java delete mode 100644 src/main/java/com/example/eightyage/global/util/.gitkeep diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 15c3a86..edf713a 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -12,6 +12,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Service @RequiredArgsConstructor public class AuthService { @@ -25,7 +27,7 @@ public class AuthService { public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { - throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); + throw new BadRequestException(PASSWORD_CONFIRMATION_MISMATCH.getMessage()); } User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword(), request.getUserRole()); @@ -39,11 +41,11 @@ public AuthTokensResponseDto signin(AuthSigninRequestDto request) { User user = userService.findUserByEmailOrElseThrow(request.getEmail()); if (user.getDeletedAt() != null) { - throw new UnauthorizedException("탈퇴한 유저 이메일입니다."); + throw new UnauthorizedException(DEACTIVATED_USER_EMAIL.getMessage()); } if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - throw new UnauthorizedException("잘못된 비밀번호입니다."); + throw new UnauthorizedException(INVALID_PASSWORD.getMessage()); } return getTokenResponse(user); @@ -63,6 +65,9 @@ private AuthTokensResponseDto getTokenResponse(User user) { String accessToken = tokenService.createAccessToken(user); String refreshToken = tokenService.createRefreshToken(user); - return new AuthTokensResponseDto(accessToken, refreshToken); + return AuthTokensResponseDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); } } diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java index 6483524..b2c3514 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Service; import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; @Service @RequiredArgsConstructor @@ -37,7 +39,7 @@ public User reissueToken(String token) { RefreshToken refreshToken = findByTokenOrElseThrow(token); if (refreshToken.getTokenState() == INVALIDATED) { - throw new UnauthorizedException("사용이 만료된 refresh token 입니다."); + throw new UnauthorizedException(EXPIRED_REFRESH_TOKEN.getMessage()); } refreshToken.updateTokenStatus(INVALIDATED); @@ -46,6 +48,6 @@ public User reissueToken(String token) { private RefreshToken findByTokenOrElseThrow(String token) { return refreshTokenRepository.findByToken(token).orElseThrow( - () -> new NotFoundException("리프레시 토큰을 찾을 수 없습니다.")); + () -> new NotFoundException(REFRESH_TOKEN_NOT_FOUND.getMessage())); } } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java index 21fce31..86adf60 100644 --- a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java +++ b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java @@ -1,13 +1,8 @@ package com.example.eightyage.domain.user.controller; -import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; -import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; -import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.dto.AuthUser; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; @@ -26,7 +21,7 @@ public class UserController { @PostMapping("/v1/users/delete") public void signup( @AuthenticationPrincipal AuthUser authUser, - @RequestBody UserDeleteRequest request + @RequestBody UserDeleteRequestDto request ) { userService.deleteUser(authUser, request); } diff --git a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java index c384dd2..7f92a4f 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java @@ -6,6 +6,8 @@ import java.util.Arrays; +import static com.example.eightyage.global.exception.ErrorMessage.NOT_INVALID_USER_ROLE; + @Getter @RequiredArgsConstructor public enum UserRole { @@ -19,7 +21,7 @@ public static UserRole of(String role) { return Arrays.stream(UserRole.values()) .filter(r -> r.getUserRole().equalsIgnoreCase(role)) .findFirst() - .orElseThrow(() -> new UnauthorizedException("유효하지 않은 UserRole")); + .orElseThrow(() -> new UnauthorizedException(NOT_INVALID_USER_ROLE.getMessage())); } public static class Authority { diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index cc9364f..68c461b 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.user.service; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; @@ -13,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Service @RequiredArgsConstructor public class UserService { @@ -25,23 +27,28 @@ public class UserService { public User saveUser(String email, String nickname, String password, String userRole) { if (userRepository.existsByEmail(email)) { - throw new BadRequestException("등록된 이메일입니다."); + throw new BadRequestException(DUPLICATE_EMAIL.getMessage()); } String encodedPassword = passwordEncoder.encode(password); - User user = new User(email, nickname, encodedPassword, UserRole.of(userRole)); + User user = User.builder() + .email(email) + .nickname(nickname) + .password(encodedPassword) + .userRole(UserRole.of(userRole)) + .build(); return userRepository.save(user); } /* 회원탈퇴 */ @Transactional - public void deleteUser(AuthUser authUser, UserDeleteRequest request) { + public void deleteUser(AuthUser authUser, UserDeleteRequestDto request) { User findUser = findUserByIdOrElseThrow(authUser.getUserId()); if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { - throw new UnauthorizedException("비밀번호가 일치하지 않습니다."); + throw new UnauthorizedException(INVALID_PASSWORD.getMessage()); } findUser.deleteUser(); @@ -49,13 +56,13 @@ public void deleteUser(AuthUser authUser, UserDeleteRequest request) { public User findUserByEmailOrElseThrow(String email) { return userRepository.findByEmail(email).orElseThrow( - () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") + () -> new UnauthorizedException(USER_EMAIL_NOT_FOUND.getMessage()) ); } public User findUserByIdOrElseThrow(Long userId) { return userRepository.findById(userId).orElseThrow( - () -> new NotFoundException("해당 유저의 Id를 찾을 수 없습니다.") + () -> new NotFoundException(USER_ID_NOT_FOUND.getMessage()) ); } } diff --git a/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java index 5a5be23..c95cc79 100644 --- a/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java +++ b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java @@ -10,6 +10,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_MUST_BE_STRING; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; + public class RefreshArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { @@ -17,7 +20,7 @@ public boolean supportsParameter(MethodParameter parameter) { boolean isStringType = parameter.getParameterType().equals(String.class); if (hasRefreshTokenAnnotation != isStringType) { - throw new UnauthorizedException("@RefreshToken과 String 타입은 함께 사용되어야 합니다."); + throw new UnauthorizedException(REFRESH_TOKEN_MUST_BE_STRING.getMessage()); } return hasRefreshTokenAnnotation; } @@ -39,6 +42,6 @@ public Object resolveArgument( } } } - throw new UnauthorizedException("리프레시 토큰이 존재하지 않습니다. 다시 로그인 해주세요."); + throw new UnauthorizedException(REFRESH_TOKEN_NOT_FOUND.getMessage()); } } diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorCode.java b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java index 8063695..a9a2fc6 100644 --- a/src/main/java/com/example/eightyage/global/exception/ErrorCode.java +++ b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java @@ -3,12 +3,14 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Getter public enum ErrorCode { - AUTHORIZATION(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), - BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), - NOT_FOUND(HttpStatus.NOT_FOUND, "찾지 못했습니다."), - FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다."); + AUTHORIZATION(HttpStatus.UNAUTHORIZED, DEFAULT_UNAUTHORIZED.getMessage()), + BAD_REQUEST(HttpStatus.BAD_REQUEST, DEFAULT_BAD_REQUEST.getMessage()), + NOT_FOUND(HttpStatus.NOT_FOUND, DEFAULT_NOT_FOUND.getMessage()), + FORBIDDEN(HttpStatus.FORBIDDEN, DEFAULT_FORBIDDEN.getMessage()); private final HttpStatus status; private final String defaultMessage; diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java new file mode 100644 index 0000000..a3adf1d --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java @@ -0,0 +1,34 @@ +package com.example.eightyage.global.exception; + +import lombok.Getter; + +@Getter +public enum ErrorMessage { + NOT_INVALID_USER_ROLE("유효하지 않은 UserRole"), + NOT_FOUND_TOKEN("토큰을 찾을 수 없습니다."), + PASSWORD_CONFIRMATION_MISMATCH("비밀번호가 비밀번호 확인과 일치하지 않습니다."), + DEACTIVATED_USER_EMAIL("탈퇴한 유저 이메일입니다."), + INVALID_PASSWORD("비밀번호가 일치하지 않습니다."), + EXPIRED_REFRESH_TOKEN("사용이 만료된 refresh token 입니다."), + REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없습니다."), + DUPLICATE_EMAIL("등록된 이메일입니다."), + USER_EMAIL_NOT_FOUND("가입한 유저의 이메일이 아닙니다."), + USER_ID_NOT_FOUND("해당 유저의 Id를 찾을 수 없습니다."), + REFRESH_TOKEN_MUST_BE_STRING("@RefreshToken과 String 타입은 함께 사용되어야 합니다."), + + DEFAULT_UNAUTHORIZED("인증이 필요합니다."), + DEFAULT_BAD_REQUEST("잘못된 요청입니다."), + DEFAULT_NOT_FOUND("찾지 못했습니다."), + DEFAULT_FORBIDDEN("권한이 없습니다."), + INTERNAL_SERVER_ERROR("서버 오류가 발생했습니다."), + + INVALID_JWT_SIGNATURE("유효하지 않는 JWT 서명입니다."), + EXPIRED_JWT_TOKEN("만료된 JWT 토큰입니다."), + UNSUPPORTED_JWT_TOKEN("지원되지 않는 JWT 토큰입니다."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 79194a0..650e95c 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -12,6 +12,8 @@ import java.util.List; +import static com.example.eightyage.global.exception.ErrorMessage.INTERNAL_SERVER_ERROR; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -37,6 +39,6 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); - return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."); + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getMessage()); } } diff --git a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java index 3a8d43b..1e626ec 100644 --- a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java @@ -21,6 +21,8 @@ import java.io.IOException; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Slf4j @Component @RequiredArgsConstructor @@ -48,18 +50,18 @@ protected void doFilterInternal( } catch (SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, INVALID_JWT_SIGNATURE.getMessage()); return; } catch (ExpiredJwtException e) { log.error("Expired JWT token, 만료된 JWT token 입니다.", e); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, EXPIRED_JWT_TOKEN.getMessage()); return; } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, UNSUPPORTED_JWT_TOKEN.getMessage()); return; } catch (Exception e) { - log.error("Internal server error", e); + log.error(INTERNAL_SERVER_ERROR.getMessage(), e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } diff --git a/src/main/java/com/example/eightyage/global/util/.gitkeep b/src/main/java/com/example/eightyage/global/util/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/global/util/JwtUtil.java b/src/main/java/com/example/eightyage/global/util/JwtUtil.java index 9aea79f..8547e15 100644 --- a/src/main/java/com/example/eightyage/global/util/JwtUtil.java +++ b/src/main/java/com/example/eightyage/global/util/JwtUtil.java @@ -16,6 +16,8 @@ import java.util.Base64; import java.util.Date; +import static com.example.eightyage.global.exception.ErrorMessage.NOT_FOUND_TOKEN; + @Slf4j(topic = "JwtUtil") @Component public class JwtUtil { @@ -53,7 +55,7 @@ public String substringToken(String tokenValue) throws ServerException { if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { return tokenValue.substring(7); } - throw new ServerException("Not Found Token"); + throw new ServerException(NOT_FOUND_TOKEN.getMessage()); } public Claims extractClaims(String token) { From 1d233150f14a335338a95a82e42915515ce6edb5 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:56:18 +0900 Subject: [PATCH 035/164] =?UTF-8?q?refactor(auth):=20AuthServiceTest=20Bui?= =?UTF-8?q?lder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/auth/service/AuthServiceTest.java | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java index 6843198..c65ea01 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -8,6 +8,7 @@ import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +19,7 @@ import java.time.LocalDateTime; +import static com.example.eightyage.global.exception.ErrorMessage.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -36,22 +38,55 @@ public class AuthServiceTest { @InjectMocks private AuthService authService; + private AuthSignupRequestDto successSignupDto; + private AuthSignupRequestDto passwordCheckErrorSignupDto; + private AuthSigninRequestDto successSigninDto; + private User user; + + @BeforeEach + public void setUp() { + passwordCheckErrorSignupDto = AuthSignupRequestDto.builder() + .email("email@email.com") + .nickname("nickname") + .password("password1234") + .passwordCheck("password1234!") + .userRole("USER_ROLE") + .build(); + + successSignupDto = AuthSignupRequestDto.builder() + .email("email@email.com") + .nickname("nickname") + .password("password1234") + .passwordCheck("password1234") + .userRole("USER_ROLE") + .build(); + + successSigninDto = AuthSigninRequestDto.builder() + .email("email@email.com") + .password("password1234") + .build(); + + user = User.builder() + .email(successSignupDto.getEmail()) + .nickname(successSignupDto.getNickname()) + .userRole(UserRole.ROLE_USER) + .build(); + + } + @Test void 회원가입_비밀번호_확인_불일치_실패() { // given - AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341", "USER_ROLE"); // when & then - assertThrows(BadRequestException.class, - () -> authService.signup(passwordCheckErrorSignupDto), - "비밀번호가 비밀번호 확인과 일치하지 않습니다."); + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> authService.signup(passwordCheckErrorSignupDto)); + assertEquals(badRequestException.getMessage(), PASSWORD_CONFIRMATION_MISMATCH.getMessage()); } @Test void 회원가입_성공() { // given - AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234", "USER_ROLE"); - User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); String accessToken = "accessToken"; String refreshToken = "refreshToken"; @@ -70,51 +105,45 @@ public class AuthServiceTest { @Test void 로그인_삭제된_유저의_이메일일_경우_실패() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", LocalDateTime.now()); given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); // when & then - assertThrows(UnauthorizedException.class, - () -> authService.signin(seccessSigninDto), - "탈퇴한 유저 이메일입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> authService.signin(successSigninDto)); + assertEquals(unauthorizedException.getMessage(), DEACTIVATED_USER_EMAIL.getMessage()); } @Test void 로그인_비밀번호가_일치하지_않을_경우_실패() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", null); given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); - given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(false); + given(passwordEncoder.matches(successSigninDto.getPassword(), user.getPassword())).willReturn(false); // when & then - assertThrows(UnauthorizedException.class, - () -> authService.signin(seccessSigninDto), - "잘못된 비밀번호입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> authService.signin(successSigninDto)); + assertEquals(unauthorizedException.getMessage(), INVALID_PASSWORD.getMessage()); } @Test void 로그인_성공() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", null); String accessToken = "accessToken"; String refreshToken = "refreshToken"; given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); - given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(true); + given(passwordEncoder.matches(successSigninDto.getPassword(), user.getPassword())).willReturn(true); given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); // when - AuthTokensResponseDto result = authService.signin(seccessSigninDto); + AuthTokensResponseDto result = authService.signin(successSigninDto); // then assertEquals(accessToken, result.getAccessToken()); @@ -124,7 +153,6 @@ public class AuthServiceTest { @Test void 토큰_재발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refreshToken"; String reissuedAccessToken = "reissued-accessToken"; From 53beecefe4ed4e6f4f245928a3e2968e72726281 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:02:35 +0900 Subject: [PATCH 036/164] =?UTF-8?q?refactor(auth):=20TokenServiceTest=20Bu?= =?UTF-8?q?ilder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/auth/service/TokenServiceTest.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java index 4ae6465..8e9492f 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -8,6 +8,7 @@ import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import com.example.eightyage.global.util.JwtUtil; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,6 +18,8 @@ import java.util.Optional; import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -36,11 +39,22 @@ public class TokenServiceTest { @InjectMocks private TokenService tokenService; + private User user; + + @BeforeEach + public void setUp() { + user = User.builder() + .email("email@email.com") + .nickname("nickname") + .userRole(UserRole.ROLE_USER) + .build(); + + } + /* createAccessToken */ @Test void 토큰발급_AccessToken_발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String accessToken = "accessToken"; given(jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole())).willReturn(accessToken); @@ -56,7 +70,6 @@ public class TokenServiceTest { @Test void 토큰발급_RefreshToken_발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); RefreshToken mockRefreshToken = new RefreshToken(user.getId()); given(refreshTokenRepository.save(any(RefreshToken.class))).willReturn(mockRefreshToken); @@ -73,7 +86,6 @@ public class TokenServiceTest { @Test void 토큰유효성검사_비활성_상태일때_실패() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refresh-token"; RefreshToken mockRefreshToken = mock(RefreshToken.class); @@ -82,9 +94,9 @@ public class TokenServiceTest { given(mockRefreshToken.getTokenState()).willReturn(INVALIDATED); // when & then - assertThrows(UnauthorizedException.class, - () -> tokenService.reissueToken(refreshToken), - "사용이 만료된 refresh token 입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> tokenService.reissueToken(refreshToken)); + assertEquals(unauthorizedException.getMessage(), EXPIRED_REFRESH_TOKEN.getMessage()); } @Test @@ -95,15 +107,14 @@ public class TokenServiceTest { given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> tokenService.reissueToken(refreshToken), - "리프레시 토큰을 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> tokenService.reissueToken(refreshToken)); + assertEquals(notFoundException.getMessage(), REFRESH_TOKEN_NOT_FOUND.getMessage()); } @Test void 토큰유효성검사_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refresh-token"; RefreshToken mockRefreshToken = mock(RefreshToken.class); From d90625c12a6f5044f4bc815924788d93d62b53f1 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:22:49 +0900 Subject: [PATCH 037/164] =?UTF-8?q?refactor(user):=20UserServiceTest=20Bui?= =?UTF-8?q?lder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/user/service/UserServiceTest.java | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java index 0fa1b70..8041ceb 100644 --- a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -1,8 +1,6 @@ package com.example.eightyage.domain.user.service; -import com.example.eightyage.domain.auth.entity.RefreshToken; -import com.example.eightyage.domain.auth.service.TokenService; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; @@ -10,6 +8,7 @@ import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -20,12 +19,11 @@ import java.util.Optional; +import static com.example.eightyage.global.exception.ErrorMessage.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) public class UserServiceTest { @@ -38,6 +36,33 @@ public class UserServiceTest { @InjectMocks private UserService userService; + private User user; + private AuthUser authUser; + private UserDeleteRequestDto successDeleteDto; + private UserDeleteRequestDto wrongPasswordDeleteDto; + + @BeforeEach + public void setUp() { + user = User.builder() + .nickname("nickname") + .userRole(UserRole.ROLE_USER) + .build(); + + authUser = AuthUser.builder() + .userId(1L) + .email("email@email.com") + .nickname("nickname") + .role(UserRole.ROLE_USER) + .build(); + + successDeleteDto = UserDeleteRequestDto.builder() + .password("correct-password") + .build(); + wrongPasswordDeleteDto = UserDeleteRequestDto.builder() + .password("wrong-password") + .build(); + } + /* findUserByIdOrElseThrow */ @Test void findById조회_userId가_없을_경우_실패() { @@ -47,16 +72,16 @@ public class UserServiceTest { given(userRepository.findById(anyLong())).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> userService.findUserByIdOrElseThrow(userId), - "해당 유저의 Id를 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> userService.findUserByIdOrElseThrow(userId)); + assertEquals(notFoundException.getMessage(), USER_ID_NOT_FOUND.getMessage()); } @Test void findById조회_성공() { // given Long userId = 1L; - User user = new User(userId, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "id", userId); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); @@ -66,7 +91,6 @@ public class UserServiceTest { // then assertNotNull(resultUser); assertEquals(user.getId(), resultUser.getId()); - assertEquals(user.getEmail(), resultUser.getEmail()); assertEquals(user.getNickname(), resultUser.getNickname()); assertEquals(user.getUserRole(), resultUser.getUserRole()); } @@ -80,16 +104,16 @@ public class UserServiceTest { given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty()); // when & then - assertThrows(UnauthorizedException.class, - () -> userService.findUserByEmailOrElseThrow(email), - "가입한 유저의 이메일이 아닙니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> userService.findUserByEmailOrElseThrow(email)); + assertEquals(unauthorizedException.getMessage(), USER_EMAIL_NOT_FOUND.getMessage()); } @Test void findByEmail조회_성공() { // given String email = "email@email.com"; - User user = new User(1L, email, "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "email", email); given(userRepository.findByEmail(any(String.class))).willReturn(Optional.of(user)); @@ -98,7 +122,6 @@ public class UserServiceTest { // then assertNotNull(resultUser); - assertEquals(user.getId(), resultUser.getId()); assertEquals(user.getEmail(), resultUser.getEmail()); assertEquals(user.getNickname(), resultUser.getNickname()); assertEquals(user.getUserRole(), resultUser.getUserRole()); @@ -116,9 +139,9 @@ public class UserServiceTest { given(userRepository.existsByEmail(any(String.class))).willReturn(true); // when & then - assertThrows(BadRequestException.class, - () -> userService.saveUser(email, nickname, password, userRole), - "등록된 이메일입니다."); + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> userService.saveUser(email, nickname, password, userRole)); + assertEquals(badRequestException.getMessage(), DUPLICATE_EMAIL.getMessage()); } @Test @@ -128,7 +151,9 @@ public class UserServiceTest { String nickname = "nickname"; String password = "password1234"; String userRole = "ROLE_USER"; - User user = new User(email, nickname, password, UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "email", "email@email.com"); + ReflectionTestUtils.setField(user, "password", "password1234"); + String encodedPassword = "encoded-password1234"; @@ -144,47 +169,40 @@ public class UserServiceTest { assertEquals(email, resultUser.getEmail()); assertEquals(nickname, resultUser.getNickname()); assertEquals(password, resultUser.getPassword()); + assertEquals(UserRole.of(userRole), resultUser.getUserRole()); + } /* deleteUser */ @Test void 회원탈퇴_회원이_존재하지_않으면_실패() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); - given(userRepository.findById(anyLong())).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> userService.deleteUser(authUser, successDeleteDto), - "해당 유저의 Id를 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> userService.deleteUser(authUser, successDeleteDto)); + assertEquals(notFoundException.getMessage(), USER_ID_NOT_FOUND.getMessage()); } @Test void 회원탈퇴_비밀번호가_일치하지_않으면_실패() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - ReflectionTestUtils.setField(user, "password", "password1234"); + ReflectionTestUtils.setField(user, "password", "correct-password"); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(false); // when & then - assertThrows(UnauthorizedException.class, - () -> userService.deleteUser(authUser, successDeleteDto), - "비밀번호가 일치하지 않습니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> userService.deleteUser(authUser, wrongPasswordDeleteDto)); + assertEquals(unauthorizedException.getMessage(), INVALID_PASSWORD.getMessage()); } @Test void 회원탈퇴_성공() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234"); - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - ReflectionTestUtils.setField(user, "password", "password1234"); + ReflectionTestUtils.setField(user, "password", "wrong-password"); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(true); @@ -194,6 +212,5 @@ public class UserServiceTest { // then assertNotNull(user.getDeletedAt()); - } } From b9dc4f44746b89e74c2c7e6baa0f8f8763a660e5 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:25:37 +0900 Subject: [PATCH 038/164] =?UTF-8?q?style(user):=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 회원수정 주석 수정 --- .../com/example/eightyage/domain/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 68c461b..fc18d0c 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -22,7 +22,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - /* 훠원저장 */ + /* 회원저장 */ @Transactional public User saveUser(String email, String nickname, String password, String userRole) { From 83b73ea73cc6b2cbafd17da9511a47ab434b1878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 22:58:57 +0900 Subject: [PATCH 039/164] =?UTF-8?q?fix:=20findProductByIdOrElseThrow=20rep?= =?UTF-8?q?ository=20=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/entity/Product.java | 2 +- .../domain/product/repository/ProductRepository.java | 11 +++++++++-- .../domain/product/service/ProductService.java | 12 +++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index b666769..549fa4c 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -14,7 +14,7 @@ @Entity @Getter @NoArgsConstructor -@Table(name="products") +@Table(name = "product") public class Product extends TimeStamped { @Id diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index 6ddba40..08ac537 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -1,8 +1,11 @@ package com.example.eightyage.domain.product.repository; import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.global.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -10,6 +13,10 @@ @Repository public interface ProductRepository extends JpaRepository { - @Query("SELECT p FROM Product p WHERE p.deletedAt IS NULL") - Optional findById(Long productId); + @Query("SELECT p FROM Product p WHERE p.id = :productId AND p.deletedAt IS NULL") + Optional findById(@Param("productId") Long productId); + + default Product findProductByIdOrElseThrow(Long productId){ + return findById(productId).orElseThrow(() -> new NotFoundException("해당 제품이 존재하지 않습니다.")); + } } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index fa6f702..b378c5c 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -34,9 +34,7 @@ public void saveProduct(String productName, Category category, String content, I // 제품 수정 @Transactional public ProductUpdateResponseDto updateProduct(Long productId, String productName, Category category, String content, SaleState saleState, Integer price) { - Product findProduct = productRepository.findById(productId).orElseThrow( - () -> new NotFoundException("존재하지 않는 상품입니다.") - ); + Product findProduct = productRepository.findProductByIdOrElseThrow(productId); if(productName != null) findProduct.setName(productName); if(category != null) findProduct.setCategory(category); @@ -50,9 +48,7 @@ public ProductUpdateResponseDto updateProduct(Long productId, String productName // 제품 단건 조회 @Transactional(readOnly = true) public ProductGetResponseDto findProductById(Long productId) { - Product findProduct = productRepository.findById(productId).orElseThrow( - () -> new NotFoundException("존재하지 않는 상품입니다.") - ); + Product findProduct = productRepository.findProductByIdOrElseThrow(productId); return new ProductGetResponseDto(findProduct.getName(), findProduct.getContent(), findProduct.getCategory(), findProduct.getPrice(), findProduct.getSaleState(), findProduct.getCreatedAt(), findProduct.getModifiedAt()); } @@ -60,9 +56,7 @@ public ProductGetResponseDto findProductById(Long productId) { // 제품 삭제 @Transactional public void deleteProduct(Long productId) { - Product findProduct = productRepository.findById(productId).orElseThrow( - () -> new NotFoundException("존재하지 않는 상품입니다.") - ); + Product findProduct = productRepository.findProductByIdOrElseThrow(productId); findProduct.setDeletedAt(LocalDateTime.now()); } From 28b5e1f8cd3374e721a5501d28a524eb7c88f964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 22:59:29 +0900 Subject: [PATCH 040/164] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1,=20=EB=A6=AC=EB=B7=B0=20=EB=8B=A4=EA=B1=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 85 +++++++++++++++++++ .../dto/request/ReviewSaveRequestDto.java | 15 ++++ .../dto/request/ReviewUpdateRequestDto.java | 14 +++ .../dto/response/ReviewSaveResponseDto.java | 27 ++++++ .../dto/response/ReviewUpdateResponseDto.java | 25 ++++++ .../dto/response/ReviewsGetResponseDto.java | 25 ++++++ .../domain/review/entity/Review.java | 39 +++++++++ .../review/repository/ReviewRepository.java | 28 ++++++ .../domain/review/service/ReviewService.java | 81 ++++++++++++++++++ 9 files changed, 339 insertions(+) create mode 100644 src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java create mode 100644 src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java create mode 100644 src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java create mode 100644 src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java create mode 100644 src/main/java/com/example/eightyage/domain/review/entity/Review.java create mode 100644 src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/review/service/ReviewService.java diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..335993a --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -0,0 +1,85 @@ +package com.example.eightyage.domain.review.controller; + +import com.example.eightyage.domain.review.dto.request.ReviewSaveRequestDto; +import com.example.eightyage.domain.review.dto.request.ReviewUpdateRequestDto; +import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewUpdateResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewsGetResponseDto; +import com.example.eightyage.domain.review.service.ReviewService; +import com.example.eightyage.global.dto.AuthUser; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + // 리뷰 생성 + @Secured("ROLE_USER") + @PostMapping("/products/{productId}/reviews") + public ResponseEntity saveReview( + @AuthenticationPrincipal AuthUser authUser, + @PathVariable Long productId, + @Valid @RequestBody ReviewSaveRequestDto requestDto + ){ + ReviewSaveResponseDto responseDto = reviewService.saveReview(authUser.getUserId(), productId, requestDto.getScore(), requestDto.getContent()); + + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } + + // 리뷰 수정 + @Secured("ROLE_USER") + @PatchMapping("/reviews/{reviewId}") + public ResponseEntity updateReview( + @AuthenticationPrincipal AuthUser authUser, + @PathVariable Long reviewId, + @Valid @RequestBody ReviewUpdateRequestDto requestDto + ){ + ReviewUpdateResponseDto responseDto = reviewService.updateReview(authUser.getUserId(), reviewId, requestDto.getScore(), requestDto.getContent()); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); + } + + // 리뷰 다건 조회 + @GetMapping("/products/{productId}/reviews") + public ResponseEntity> getReviews( + @PathVariable Long productId, + @RequestParam(required = false, defaultValue = "score") String orderBy, + @PageableDefault(size = 10) Pageable pageable + ){ + Pageable sortedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + Sort.by(Sort.Direction.DESC, orderBy) + ); + + Page reviews = reviewService.findReviews(productId, sortedPageable); + + return new ResponseEntity<>(reviews, HttpStatus.OK); + } + + // 리뷰 삭제 + @Secured("ROLE_USER") + @DeleteMapping("/reviews/{reviewId}") + public ResponseEntity deleteReview( + @AuthenticationPrincipal AuthUser authUser, + @PathVariable Long reviewId + ){ + reviewService.deleteReview(authUser.getUserId(), reviewId); + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java new file mode 100644 index 0000000..4a5e3d4 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java @@ -0,0 +1,15 @@ +package com.example.eightyage.domain.review.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class ReviewSaveRequestDto { + + @NotNull(message = "반드시 값이 있어야 합니다.") + private Double score; + + @NotBlank(message = "반드시 값이 있어야 합니다.") + private String content; +} diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java new file mode 100644 index 0000000..f6f0f0b --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -0,0 +1,14 @@ +package com.example.eightyage.domain.review.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +@Getter +public class ReviewUpdateRequestDto { + + @NotNull(message = "반드시 값이 있어야 합니다.") + private Double score; + + @NotBlank(message = "반드시 값이 있어야 합니다.") + private String content; +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java new file mode 100644 index 0000000..0602f73 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.review.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ReviewSaveResponseDto { + + private final Long id; + + private final Long userId; + + private final Long productId; + + private final String nickname; + + private final Double score; + + private final String content; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java new file mode 100644 index 0000000..ea21f04 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java @@ -0,0 +1,25 @@ +package com.example.eightyage.domain.review.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ReviewUpdateResponseDto { + + private final Long id; + + private final Long userId; + + private final String nickname; + + private final Double score; + + private final String content; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java new file mode 100644 index 0000000..0af7647 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java @@ -0,0 +1,25 @@ +package com.example.eightyage.domain.review.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ReviewsGetResponseDto { + + private final Long id; + + private final Long userId; + + private final String nickname; + + private final Double score; + + private final String content; + + private final LocalDateTime createdAt; + + private final LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/example/eightyage/domain/review/entity/Review.java b/src/main/java/com/example/eightyage/domain/review/entity/Review.java new file mode 100644 index 0000000..8bb4f09 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/entity/Review.java @@ -0,0 +1,39 @@ +package com.example.eightyage.domain.review.entity; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "review") +public class Review extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Setter private Double score; + + @Setter private String content; + + public Review(User user, Product product, Double score, String content) { + this.user = user; + this.product = product; + this.score = score; + this.content = content; + } +} diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java new file mode 100644 index 0000000..9422b06 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -0,0 +1,28 @@ +package com.example.eightyage.domain.review.repository; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.global.exception.NotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReviewRepository extends JpaRepository { + + @Query("SELECT r FROM Review r WHERE r.id = :reviewId AND r.deletedAt IS NULL") + Optional findById(@Param("reviewId") Long reviewId); + + default Review findReviewByIdOrElseThrow(Long reviewId){ + return findById(reviewId).orElseThrow(() -> new NotFoundException("해당 리뷰가 존재하지 않습니다.")); + } + + Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); + +} diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java new file mode 100644 index 0000000..9ba5475 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -0,0 +1,81 @@ +package com.example.eightyage.domain.review.service; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewUpdateResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewsGetResponseDto; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final UserService userService; + private final ProductRepository productRepository; + + // 리뷰 생성 + @Transactional + public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double score, String content) { + User findUser = userService.findUserByIdOrElseThrow(userId); + Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + + Review review = new Review(findUser, findProduct, score, content); + Review savedReview = reviewRepository.save(review); + + return new ReviewSaveResponseDto(savedReview.getId(), findUser.getId(), findProduct.getId(), findUser.getNickname(), savedReview.getScore(), savedReview.getContent(), savedReview.getCreatedAt(), savedReview.getModifiedAt()); + } + + // 리뷰 수정 + @Transactional + public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double score, String content) { + User findUser = userService.findUserByIdOrElseThrow(userId); + Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); + + if(findUser.getId() == findReview.getUser().getId()){ + if(content != null) findReview.setContent(content); + if(score != null) findReview.setScore(score); + } + + return new ReviewUpdateResponseDto(findReview.getId(), userId, findUser.getNickname(), findReview.getScore(), findReview.getContent(), findReview.getCreatedAt(), findReview.getModifiedAt()); + } + + // 리뷰 다건 조회 + @Transactional(readOnly = true) + public Page findReviews(Long productId, Pageable pageable) { + Page reviewPage = reviewRepository.findByProductIdAndProductDeletedAtIsNull(productId, pageable); + + return reviewPage.map(review -> new ReviewsGetResponseDto( + review.getId(), + review.getUser().getId(), + review.getUser().getNickname(), + review.getScore(), + review.getContent(), + review.getCreatedAt(), + review.getModifiedAt() + )); + } + + // 리뷰 삭제 + @Transactional + public void deleteReview(Long userId, Long reviewId) { + User findUser = userService.findUserByIdOrElseThrow(userId); + Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); + + if(findUser.getId() == findReview.getUser().getId()){ + findReview.setDeletedAt(LocalDateTime.now()); + } + } +} From b0a5003eff64627b2b154073a6fa46a754869fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 22:59:53 +0900 Subject: [PATCH 041/164] =?UTF-8?q?chore:=20.gitkeep=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/eightyage/domain/review/controller/.gitkeep | 0 .../java/com/example/eightyage/domain/review/dto/request/.gitkeep | 0 .../com/example/eightyage/domain/review/dto/response/.gitkeep | 0 src/main/java/com/example/eightyage/domain/review/entity/.gitkeep | 0 .../java/com/example/eightyage/domain/review/repository/.gitkeep | 0 .../java/com/example/eightyage/domain/review/service/.gitkeep | 0 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/review/controller/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/review/dto/request/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/review/dto/response/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/review/entity/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/review/repository/.gitkeep delete mode 100644 src/main/java/com/example/eightyage/domain/review/service/.gitkeep diff --git a/src/main/java/com/example/eightyage/domain/review/controller/.gitkeep b/src/main/java/com/example/eightyage/domain/review/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/.gitkeep b/src/main/java/com/example/eightyage/domain/review/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/.gitkeep b/src/main/java/com/example/eightyage/domain/review/dto/response/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/review/entity/.gitkeep b/src/main/java/com/example/eightyage/domain/review/entity/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/review/repository/.gitkeep b/src/main/java/com/example/eightyage/domain/review/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/review/service/.gitkeep b/src/main/java/com/example/eightyage/domain/review/service/.gitkeep deleted file mode 100644 index e69de29..0000000 From 65039b73bc16252a335abfa438a2d290819e35f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 23:04:00 +0900 Subject: [PATCH 042/164] =?UTF-8?q?feat:=20Product=20Validation=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 5 +++-- .../product/dto/request/ProductUpdateRequestDto.java | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 28df14a..19b88a1 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -6,6 +6,7 @@ import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.service.ProductService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -22,7 +23,7 @@ public class ProductController { // 제품 생성 @Secured("ROLE_ADMIN") @PostMapping - public ResponseEntity saveProduct(@RequestBody ProductSaveRequestDto requestDto){ + public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDto requestDto){ productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); return new ResponseEntity<>(HttpStatus.CREATED); @@ -33,7 +34,7 @@ public ResponseEntity saveProduct(@RequestBody ProductSaveRequestDto reque @PatchMapping("/{productId}") public ResponseEntity updateProduct( @PathVariable Long productId, - @RequestBody ProductUpdateRequestDto requestDto + @Valid @RequestBody ProductUpdateRequestDto requestDto ){ ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getSaleState(), requestDto.getPrice()); diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java index 39e8e19..978880d 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -2,19 +2,25 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; @Getter public class ProductUpdateRequestDto { - + @NotBlank(message="반드시 값이 있어야 합니다.") private String productName; + @NotBlank(message="반드시 값이 있어야 합니다.") private Category category; + @NotBlank(message="반드시 값이 있어야 합니다.") private String content; + @NotBlank(message="반드시 값이 있어야 합니다.") private SaleState saleState; + @NotNull(message="반드시 값이 있어야 합니다.") private Integer price; } From be984512efc8ae628800b6be05365265c88cb4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 25 Mar 2025 23:13:39 +0900 Subject: [PATCH 043/164] =?UTF-8?q?feat:=20=EC=A0=9C=ED=92=88=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=98=90=ED=95=9C=20=EC=A7=80=EC=9B=8C=EC=A7=80=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/service/ProductService.java | 9 +++++++++ .../domain/review/repository/ReviewRepository.java | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index b378c5c..2620ad1 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -7,6 +7,8 @@ import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.entity.SaleState; import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; @@ -14,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -22,6 +25,7 @@ public class ProductService { private final ProductRepository productRepository; + private final ReviewRepository reviewRepository; // 제품 생성 @Transactional @@ -57,6 +61,11 @@ public ProductGetResponseDto findProductById(Long productId) { @Transactional public void deleteProduct(Long productId) { Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + List findReviewList = reviewRepository.findReviewsByProductId(productId); + + for(Review review : findReviewList){ + review.setDeletedAt(LocalDateTime.now()); + } findProduct.setDeletedAt(LocalDateTime.now()); } diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java index 9422b06..d02dccf 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -25,4 +25,6 @@ default Review findReviewByIdOrElseThrow(Long reviewId){ Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); + @Query("SELECT r FROM Review r WHERE r.product.id = :productId") + List findReviewsByProductId(@Param("productId") Long productId); } From b5a04cd274753e7e827ae83867707bf8b96f6967 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 23:48:35 +0900 Subject: [PATCH 044/164] =?UTF-8?q?refactor(auth):=20ValidationMessage=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - enum으로 작성하고 싶었으나 메서드 단계가 아니라 사용 불가 - 따라서 상수를 저장하는 클래스로 하드코딩 방지 --- .../auth/dto/request/AuthSignupRequestDto.java | 14 ++++++++------ .../eightyage/global/dto/ValidationMessage.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/dto/ValidationMessage.java diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index ad3680c..1dad7d2 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -7,21 +7,23 @@ import lombok.Builder; import lombok.Getter; +import static com.example.eightyage.global.dto.ValidationMessage.*; + @Getter @Builder @AllArgsConstructor public class AuthSignupRequestDto { - @NotBlank(message = "이메일은 필수 입력 값입니다.") - @Email(message = "이메일 형식으로 입력되어야 합니다.") + @NotBlank(message = NOT_BLANK_EMAIL) + @Email(message = PATTERN_EMAIL) private String email; - @NotBlank(message = "닉네임은 필수 입력 값입니다.") + @NotBlank(message = NOT_BLANK_NICKNAME) private String nickname; - @NotBlank(message = "비밀번호는 필수 입력 값입니다.") - @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", - message = "비밀번호는 영어, 숫자 포함 8자리 이상이어야 합니다.") + @NotBlank(message = NOT_BLANK_PASSWORD) + @Pattern(regexp = PATTERN_PASSWORD_REGEXP, + message = PATTERN_PASSWORD) private String password; private String passwordCheck; diff --git a/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java new file mode 100644 index 0000000..ea339d6 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java @@ -0,0 +1,16 @@ +package com.example.eightyage.global.dto; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ValidationMessage { + + public static final String NOT_BLANK_EMAIL = "이메일은 필수 입력 값입니다."; + public static final String PATTERN_EMAIL = "이메일 형식으로 입력되어야 합니다."; + public static final String NOT_BLANK_NICKNAME = "닉네임은 필수 입력 값입니다."; + public static final String NOT_BLANK_PASSWORD = "비밀번호는 필수 입력 값입니다."; + public static final String PATTERN_PASSWORD = "비밀번호는 영어, 숫자 포함 8자리 이상이어야 합니다."; + public static final String PATTERN_PASSWORD_REGEXP = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"; + +} \ No newline at end of file From 480fa9810acd7ab4b1fab39703d504c655f8897d Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Tue, 25 Mar 2025 16:46:08 +0900 Subject: [PATCH 045/164] =?UTF-8?q?feat(search):=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=9D=B8=EA=B8=B0=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java # src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java # src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java # src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java --- build.gradle | 4 ++ .../domain/search/dto/PopularKeywordDto.java | 2 +- .../domain/search/entity/KeywordCount.java | 25 +++++++ .../domain/search/entity/SearchLog.java | 6 +- .../controller/ProductControllerV1.java} | 23 ++----- .../service/ProductServiceV1.java} | 13 ++-- .../v2/controller/ProductControllerV2.java | 28 ++++++++ .../v2/service/ProductServiceV2.java | 36 ++++++++++ .../repository/KeywordCountRepository.java | 8 +++ .../v1/controller/SearchControllerV1.java | 26 +++++++ .../v1/service/PopularKeywordServiceV1.java | 29 ++++++++ .../v2/controller/SearchControllerV2.java | 27 ++++++++ .../v2/service/KeywordCountFlushService.java | 67 +++++++++++++++++++ .../v2/service/PopularKeywordServiceV2.java | 31 +++++++++ .../search/v2/service/SearchService.java | 60 +++++++++++++++++ .../eightyage/global/config/CacheConfig.java | 51 ++++++++++++++ 16 files changed, 409 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java rename src/main/java/com/example/eightyage/domain/search/fakeProduct/{controller/ProductController.java => v1/controller/ProductControllerV1.java} (52%) rename src/main/java/com/example/eightyage/domain/search/fakeProduct/{service/ProductService.java => v1/service/ProductServiceV1.java} (78%) create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java create mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java create mode 100644 src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java create mode 100644 src/main/java/com/example/eightyage/global/config/CacheConfig.java diff --git a/build.gradle b/build.gradle index 1524b0a..deda3fc 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,10 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + // cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + // spring cloud AWS S3 // implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' } diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java index 10d6cbe..dfd6507 100644 --- a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -8,6 +8,6 @@ public class PopularKeywordDto { private String keyword; - private long count; + private Long count; } diff --git a/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java b/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java new file mode 100644 index 0000000..6ceada2 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java @@ -0,0 +1,25 @@ +package com.example.eightyage.domain.search.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class KeywordCount { + + @Id + private String keyword; + private Long count; + + public KeywordCount(String keyword, Long count) { + this.keyword = keyword; + this.count = count; + } + + public void updateCount(Long count) { + this.count = count; + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java index 1d88ca4..89992cf 100644 --- a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java +++ b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java @@ -1,17 +1,20 @@ package com.example.eightyage.domain.search.entity; +import jakarta.persistence.*; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Entity @NoArgsConstructor @Getter +@EntityListeners(AuditingEntityListener.class) public class SearchLog { @Id @@ -19,12 +22,13 @@ public class SearchLog { private Long id; private String keyword; + + @CreatedDate private LocalDateTime searchedAt; public static SearchLog of(String keyword) { SearchLog log = new SearchLog(); log.keyword = keyword; - log.searchedAt = LocalDateTime.now(); return log; } } diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java similarity index 52% rename from src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java rename to src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java index 458da19..14a8295 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java @@ -1,8 +1,8 @@ -package com.example.eightyage.domain.search.fakeProduct.controller; +package com.example.eightyage.domain.search.fakeProduct.v1.controller; import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.service.ProductService; +import com.example.eightyage.domain.search.fakeProduct.v1.service.ProductServiceV1; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -13,9 +13,9 @@ @RestController @RequiredArgsConstructor -public class ProductController { +public class ProductControllerV1 { - private final ProductService productService; + private final ProductServiceV1 productService; @GetMapping("/api/v1/products") public ResponseEntity> searchProduct( @@ -26,19 +26,4 @@ public ResponseEntity> searchProduct( ) { return ResponseEntity.ok(productService.getProducts(name, category, size, page)); } - -// @GetMapping("/api/v1/products") -// public ResponseEntity> searchProduct( -// @RequestParam(required = false) String name, -// @RequestParam(required = false) Category category, -// @RequestParam(defaultValue = "10") int size, -// @RequestParam(defaultValue = "1") int page -// ) { -// Page results = productService.getProducts(name, category, size, page); -// if (StringUtils.hasText(name) && !results.isEmpty()){ -// searchService.saveKeyword(name); -// } -// return ResponseEntity.ok(results); -// } - } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java similarity index 78% rename from src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java rename to src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java index 3ba8ed3..2f914fd 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java @@ -1,37 +1,38 @@ -package com.example.eightyage.domain.search.fakeProduct.service; +package com.example.eightyage.domain.search.fakeProduct.v1.service; import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; import com.example.eightyage.domain.search.fakeProduct.entity.Category; import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; -import com.example.eightyage.domain.search.service.SearchService; +import com.example.eightyage.domain.search.v2.service.SearchService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor -public class ProductService { +public class ProductServiceV1 { private final ProductRepository productRepository; private final SearchService searchService; - @Transactional(readOnly = true) + @Transactional(propagation = Propagation.REQUIRES_NEW) public Page getProducts(String productName, Category category, int size, int page) { int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); Page products = productRepository.findProducts(productName, category, pageable); if (StringUtils.hasText(productName) && !products.isEmpty()) { - searchService.saveKeyword(productName); + searchService.saveSearchLog(productName); } return products.map(ProductSearchResponse::from); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java new file mode 100644 index 0000000..01acea1 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java @@ -0,0 +1,28 @@ +package com.example.eightyage.domain.search.fakeProduct.v2.controller; + +import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; +import com.example.eightyage.domain.search.fakeProduct.entity.Category; +import com.example.eightyage.domain.search.fakeProduct.v2.service.ProductServiceV2; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +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; + +@RestController +@RequiredArgsConstructor +public class ProductControllerV2 { + + private final ProductServiceV2 productService; + + @GetMapping("/api/v2/products") + public ResponseEntity> searchProduct( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProducts(name, category, size, page)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java new file mode 100644 index 0000000..a198177 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.search.fakeProduct.v2.service; + +import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; +import com.example.eightyage.domain.search.fakeProduct.entity.Category; +import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; +import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; +import com.example.eightyage.domain.search.v2.service.SearchService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +@Service +@RequiredArgsConstructor +public class ProductServiceV2 { + + private final ProductRepository productRepository; + private final SearchService searchService; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Page getProducts(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page products = productRepository.findProducts(productName, category, pageable); + + if (StringUtils.hasText(productName) && !products.isEmpty()) { + searchService.logAndCountKeyword(productName); + } + + return products.map(ProductSearchResponse::from); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java b/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java new file mode 100644 index 0000000..0341ab8 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java @@ -0,0 +1,8 @@ +package com.example.eightyage.domain.search.repository; + + +import com.example.eightyage.domain.search.entity.KeywordCount; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface KeywordCountRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java b/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java new file mode 100644 index 0000000..29b5efd --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java @@ -0,0 +1,26 @@ +package com.example.eightyage.domain.search.v1.controller; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.v1.service.PopularKeywordServiceV1; +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 java.util.List; + +@RestController +@RequiredArgsConstructor +public class SearchControllerV1 { + + private final PopularKeywordServiceV1 popularKeywordService; + + // 인기 검색어 조회 (캐시 X) + @GetMapping("/api/v1/search/popular") + public ResponseEntity> searchPopularKeywords( + @RequestParam(defaultValue = "7") int days + ) { + return ResponseEntity.ok(popularKeywordService.searchPoplarKeywords(days)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java b/src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java new file mode 100644 index 0000000..a29caf6 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.search.v1.service; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import com.example.eightyage.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PopularKeywordServiceV1 { + + private final SearchLogRepository searchLogRepository; + + // 캐시X 인기 검색어 조회 + @Transactional + public List searchPoplarKeywords(int days) { + if (days < 0 || days > 365) { + throw new BadRequestException("조회 기간은 1 ~ 365일 사이여야 합니다."); + } + LocalDateTime since = LocalDateTime.now().minusDays(days); + return searchLogRepository.findPopularKeywords(since); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java b/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java new file mode 100644 index 0000000..2188f23 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.search.v2.controller; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.v2.service.PopularKeywordServiceV2; +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 java.util.List; + +@RestController +@RequiredArgsConstructor +public class SearchControllerV2 { + + private final PopularKeywordServiceV2 popularKeywordService; + + // 인기 검색어 조회 (캐시 O) + @GetMapping("/api/v2/search/popular") + public ResponseEntity> searchPopularKeywordsV2( + @RequestParam(defaultValue = "7") int days + ) { + return ResponseEntity.ok(popularKeywordService.searchPopularKeywordsV2(days)); + } + +} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java new file mode 100644 index 0000000..2bb34b0 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java @@ -0,0 +1,67 @@ +package com.example.eightyage.domain.search.v2.service; + +import com.example.eightyage.domain.search.repository.KeywordCountRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Slf4j +public class KeywordCountFlushService { + + private final CacheManager cacheManager; + private final KeywordCountRepository keywordCountRepository; + + @Transactional + @Scheduled(fixedRate = 5 * 60 * 1000) // 5분마다 실행 + public void flushKeywordCounts() { + Cache countCache = cacheManager.getCache("keywordCountMap"); + Cache keySetCache = cacheManager.getCache("keywordKeySet"); + + if (countCache == null || keySetCache == null) { + log.warn("캐시를 찾을 수 없습니다."); + return; + } + + try { + // 키 목록 가져오기 + Set keywordSet = keySetCache.get("keywords", Set.class); + if (keywordSet == null || keywordSet.isEmpty()) { + log.info("flush 할 키워드가 없습니다."); + return; + } + + int flushed = 0; + + // 반복문을 이용하여 저장하기 + for (String keyword : keywordSet) { + Long count = countCache.get(keyword, Long.class); + if (count == null || count == 0L) continue; + + keywordCountRepository.findById(keyword) + .ifPresentOrElse( + exist -> exist.updateCount(exist.getCount() + count), + () -> keywordCountRepository.save(new com.example.eightyage.domain.search.entity.KeywordCount(keyword, count)) + ); + flushed++; + countCache.evict(keyword); + } + + keySetCache.put("keywords", new java.util.HashSet<>()); + + log.info("{}개의 키워드 플러시 성공", flushed); + + } catch (Exception e) { + log.error("플러시 실패", e); + } + + } + +} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java b/src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java new file mode 100644 index 0000000..a1c7f8d --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java @@ -0,0 +1,31 @@ +package com.example.eightyage.domain.search.v2.service; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import com.example.eightyage.global.exception.BadRequestException; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PopularKeywordServiceV2 { + + private final SearchLogRepository searchLogRepository; + + //캐시O 인기 검색어 조회 + @Transactional + @Cacheable(value = "popularKeywords", key = "#days") + public List searchPopularKeywordsV2(int days) { + if (days < 1 || days > 365) { + throw new BadRequestException("조회 일 수는 1~365 사이여야 합니다."); + } + LocalDateTime since = LocalDateTime.now().minusDays(days); + return searchLogRepository.findPopularKeywords(since); + } + +} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java b/src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java new file mode 100644 index 0000000..6cbad9c --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java @@ -0,0 +1,60 @@ +package com.example.eightyage.domain.search.v2.service; + +import com.example.eightyage.domain.search.entity.SearchLog; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.HashSet; +import java.util.Set; + + +@Service +@RequiredArgsConstructor +public class SearchService { + + private final SearchLogRepository searchLogRepository; + private final CacheManager cacheManager; + + // 검색 키워드를 로그에 저장 + @Transactional + public void saveSearchLog(String keyword) { + if (StringUtils.hasText(keyword)) { + searchLogRepository.save(SearchLog.of(keyword)); + } + } + + // 검색 시 키워드 카운트 증가 + public void increaseKeywordCount(String keyword) { + if (!StringUtils.hasText(keyword)) return; + + Cache countCache = cacheManager.getCache("keywordCountMap"); + Cache keySetCache = cacheManager.getCache("keywordKeySet"); + + if (countCache != null) { + Long count = countCache.get(keyword, Long.class); + count = (count == null) ? 1L : count + 1; + countCache.put(keyword, count); + } + + if (keySetCache != null) { + Set keywordSet = keySetCache.get("keywords", Set.class); + if (keywordSet == null) { + keywordSet = new HashSet<>(); + } + keywordSet.add(keyword); + keySetCache.put("keywords", keywordSet); + } + } + + @Transactional + public void logAndCountKeyword(String keyword) { + saveSearchLog(keyword); + increaseKeywordCount(keyword); + } + +} diff --git a/src/main/java/com/example/eightyage/global/config/CacheConfig.java b/src/main/java/com/example/eightyage/global/config/CacheConfig.java new file mode 100644 index 0000000..08f73ff --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/CacheConfig.java @@ -0,0 +1,51 @@ +package com.example.eightyage.global.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +@EnableCaching +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + + // 키워드를 카운팅하는 캐시 + CaffeineCache keywordCountMap = new CaffeineCache( + "keywordCountMap", + Caffeine.newBuilder() + .maximumSize(10000) + .build() + ); + + // 인기 검색어를 조회하는 캐시 + CaffeineCache popularKeywords = new CaffeineCache( + "popularKeywords", + Caffeine.newBuilder() + .maximumSize(365) // days 값 기준으로 최대 365개 + .expireAfterWrite(5, TimeUnit.MINUTES) // TTL 5분 + .build() + ); + + // 현재 캐시에 저장된 키워드 목록 + CaffeineCache keywordKeySet = new CaffeineCache( + "keywordKeySet", + Caffeine.newBuilder() + .maximumSize(1) + .build() + ); + + cacheManager.setCaches(Arrays.asList(keywordCountMap, popularKeywords, keywordKeySet)); + return cacheManager; + } + +} From a221a6642a7b8d7a843d2f45928236157c8eb66b Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 12:03:09 +0900 Subject: [PATCH 046/164] =?UTF-8?q?chore(ci)=20:=20Gradle=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20=EA=B5=AC=EC=B6=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ci.yml 파일 생성 - PR 발생 시 자동으로 빌드 및 테스트가 실행되도록 구현 - gradle/gradle-build-action을 적용해 Gradle 캐시 활용 - JDK 17로 빌드 환경 구성 - 대상 브랜치는 dev, main으로 설정 --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2b849e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Java CI with Gradle + +on: + pull_request: + branches: [ "dev", "main" ] # dev, main 모두 PR 대상 + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build and Test with Gradle + uses: gradle/gradle-build-action@v3 + with: + arguments: clean build \ No newline at end of file From 0351fc4ea799e8a4d37c9b73a93d270a6e8012f3 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 13:45:15 +0900 Subject: [PATCH 047/164] =?UTF-8?q?fix(ci)=20:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - JWT_SECRET_KEY 설정 - gradle/gradle-build-action이 deprecated 되어 gradle/actions/setup-gradle로 수정 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b849e..89c1fe0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ permissions: jobs: build: runs-on: ubuntu-latest + env: + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} steps: - uses: actions/checkout@v4 @@ -21,6 +23,6 @@ jobs: distribution: 'temurin' - name: Build and Test with Gradle - uses: gradle/gradle-build-action@v3 + uses: gradle/actions/setup-gradle@v3 with: arguments: clean build \ No newline at end of file From 512a247aa36e036a7c94c6d8067149026a03f488 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 14:55:33 +0900 Subject: [PATCH 048/164] =?UTF-8?q?fix(ci)=20:=20CI=EC=9A=A9=20application?= =?UTF-8?q?-ci.yml=20=EC=B6=94=EA=B0=80=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 추가 - Docker MySQL 사용하도록 수정 --- .github/workflows/ci.yml | 23 +++++++++++++++++------ src/main/resources/application-ci.yml | 10 ++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/application-ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89c1fe0..c69ca97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,16 @@ permissions: jobs: build: runs-on: ubuntu-latest - env: - JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root # Docker 내부 비밀번호 지정 + MYSQL_DATABASE: team8_test # Docker 내부 DB 이름 + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping --silent" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v4 @@ -22,7 +30,10 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Build and Test with Gradle - uses: gradle/actions/setup-gradle@v3 - with: - arguments: clean build \ No newline at end of file + - name: Wait for MySQL to be ready + run: sleep 15 + + - name: Run Test with Gradle + env: + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + run: ./gradlew clean test -Dspring.profiles.active=ci \ No newline at end of file diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml new file mode 100644 index 0000000..6fcaf65 --- /dev/null +++ b/src/main/resources/application-ci.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/team8_test + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + +jwt: + secret: + key: ${JWT_SECRET_KEY} \ No newline at end of file From a52d19b8724d2ed1973d508e41e55d5af39fb4da Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 17:35:30 +0900 Subject: [PATCH 049/164] =?UTF-8?q?fix(ci)=20:=20CI=EC=9A=A9=20application?= =?UTF-8?q?-ci.yml=20=EC=B6=94=EA=B0=80=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 추가 - Docker MySQL 사용하도록 수정 --- .github/workflows/ci.yml | 13 ++++++++++++- src/main/resources/application-ci.yml | 15 +++++++++++++-- .../eightyage/EightyageApplicationTests.java | 13 ------------- 3 files changed, 25 insertions(+), 16 deletions(-) delete mode 100644 src/test/java/com/example/eightyage/EightyageApplicationTests.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c69ca97..3aab739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,18 @@ jobs: distribution: 'temurin' - name: Wait for MySQL to be ready - run: sleep 15 + run: | + for i in {1..10}; do + if mysql -hmysql -P3306 -uroot -proot -e "SELECT 1"; then + echo "MySQL is up!" + break + fi + echo "Waiting for MySQL..." + sleep 5 + done + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew - name: Run Test with Gradle env: diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 6fcaf65..76a6120 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -1,10 +1,21 @@ +server: + port: 8080 + spring: datasource: - url: jdbc:mysql://localhost:3306/team8_test + url: jdbc:mysql://mysql:3306/team8_test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + show_sql: true + format_sql: true jwt: secret: - key: ${JWT_SECRET_KEY} \ No newline at end of file + key: vA1z3L5pQ8sK0yHdWfJdXhMTJpS9gA2fByKr6B+3UkE= \ No newline at end of file diff --git a/src/test/java/com/example/eightyage/EightyageApplicationTests.java b/src/test/java/com/example/eightyage/EightyageApplicationTests.java deleted file mode 100644 index d2e270e..0000000 --- a/src/test/java/com/example/eightyage/EightyageApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.eightyage; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class EightyageApplicationTests { - - @Test - void contextLoads() { - } - -} From bd83d4b5947d946ba4eb6a0d7f9836ff7a6b3ae5 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 17:40:17 +0900 Subject: [PATCH 050/164] =?UTF-8?q?refactor(ci)=20:=20application-ci.yml?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - application-ci.yml 수정 --- src/main/resources/application-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 76a6120..fb7a102 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -18,4 +18,4 @@ spring: format_sql: true jwt: secret: - key: vA1z3L5pQ8sK0yHdWfJdXhMTJpS9gA2fByKr6B+3UkE= \ No newline at end of file + key: ${JWT_SECRET_KEY} \ No newline at end of file From 87e7336eac9c8915cb2c079b2e7df52e4886fe15 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Tue, 25 Mar 2025 19:37:59 +0900 Subject: [PATCH 051/164] =?UTF-8?q?refactor(ci)=20:=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EC=A7=84=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정사항 - ./gradlew clean test에서 ./gradlew clean build로 수정 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aab739..19cb1c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: services: mysql: - image: mysql:8.0 + image: mysql:8.0 # MySQL 컨테이너 띄우기 env: MYSQL_ROOT_PASSWORD: root # Docker 내부 비밀번호 지정 MYSQL_DATABASE: team8_test # Docker 내부 DB 이름 @@ -44,7 +44,7 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: Run Test with Gradle + - name: Test And Build with Gradle env: JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - run: ./gradlew clean test -Dspring.profiles.active=ci \ No newline at end of file + run: ./gradlew clean build -Dspring.profiles.active=ci \ No newline at end of file From b7c6a38f5f9eab362ebc3d55eca10ba7c22585e2 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 11:51:25 +0900 Subject: [PATCH 052/164] =?UTF-8?q?feat(user):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EA=B5=AC=ED=98=84=20#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 내용 - soft delete를 사용해 deleteAt에 현재시간으로 업데이트 - 만약 deletedAt에 현재시간이 있으면 삭제된 정보임 - 비밀번호를 통해 회원탈퇴 가능 - 탈퇴한 이메일로 로그인 불가능 --- .../domain/auth/service/AuthService.java | 5 +++ .../user/controller/UserController.java | 33 +++++++++++++++++++ .../user/dto/request/UserDeleteRequest.java | 10 ++++++ .../eightyage/domain/user/entity/User.java | 19 +++++++++++ .../user/repository/UserRepository.java | 12 +++++-- .../domain/user/service/UserService.java | 17 ++++++++++ .../eightyage/global/entity/TimeStamped.java | 2 ++ 7 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/user/controller/UserController.java create mode 100644 src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index d69456b..0340b6c 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -21,6 +21,7 @@ public class AuthService { private final PasswordEncoder passwordEncoder; /* 회원가입 */ + @Transactional public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { @@ -37,6 +38,10 @@ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { public AuthTokensResponseDto signin(AuthSigninRequestDto request) { User user = userService.findUserByEmailOrElseThrow(request.getEmail()); + if (user.getDeletedAt() != null) { + throw new UnauthorizedException("탈퇴한 유저 이메일입니다."); + } + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new UnauthorizedException("잘못된 비밀번호입니다."); } diff --git a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java new file mode 100644 index 0000000..21fce31 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java @@ -0,0 +1,33 @@ +package com.example.eightyage.domain.user.controller; + +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.dto.AuthUser; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class UserController { + + private final UserService userService; + + /* 회원탈퇴 */ + @PostMapping("/v1/users/delete") + public void signup( + @AuthenticationPrincipal AuthUser authUser, + @RequestBody UserDeleteRequest request + ) { + userService.deleteUser(authUser, request); + } +} diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java new file mode 100644 index 0000000..06edae2 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java @@ -0,0 +1,10 @@ +package com.example.eightyage.domain.user.dto.request; + +import lombok.Getter; + +@Getter +public class UserDeleteRequest { + + private String password; + +} diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index bc109b4..2809145 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -1,10 +1,13 @@ package com.example.eightyage.domain.user.entity; +import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Getter @Entity @NoArgsConstructor @@ -30,4 +33,20 @@ public User(String email, String nickname, String password) { this.password = password; this.userRole = UserRole.ROLE_USER; } + + public User(Long id, String email, String nickname, UserRole userRole) { + this.id = id; + this.email = email; + this.nickname = nickname; + this.userRole = userRole; + } + + public static User fromAuthUser(AuthUser authUser) { + return new User(authUser.getUserId(), authUser.getEmail(), authUser.getEmail(), + UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())); + } + + public void deleteUser() { + setDeletedAt(LocalDateTime.now()); + } } diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java index 6ab9a06..fa2023e 100644 --- a/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserRepository.java @@ -2,11 +2,19 @@ import com.example.eightyage.domain.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); - Optional findByEmail(String email); -} + Optional findByEmail(@Param("email") String email); + + @Query("SELECT u FROM User u " + + "WHERE u.id = :userId " + + "AND u.deletedAt IS NULL") + Optional findById(@Param("userId") Long id); +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index fec30fc..6b07a80 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -1,13 +1,16 @@ package com.example.eightyage.domain.user.service; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -16,6 +19,8 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + /* 훠원저장 */ + @Transactional public User saveUser(String email, String nickname, String password) { if (userRepository.existsByEmail(email)) { @@ -29,6 +34,18 @@ public User saveUser(String email, String nickname, String password) { return userRepository.save(user); } + /* 회원탈퇴 */ + @Transactional + public void deleteUser(AuthUser authUser, UserDeleteRequest request) { + User findUser = findUserByIdOrElseThrow(authUser.getUserId()); + + if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { + throw new BadRequestException("비밀번호가 일치하지 않습니다."); + } + + findUser.deleteUser(); + } + public User findUserByEmailOrElseThrow(String email) { return userRepository.findByEmail(email).orElseThrow( () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index 5b5e58d..9bec7c0 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -23,6 +24,7 @@ public abstract class TimeStamped { @Temporal(TemporalType.TIMESTAMP) private LocalDateTime modifiedAt; + @Setter @Column @Temporal(TemporalType.TIMESTAMP) private LocalDateTime deletedAt; From 4f1709f05f4e9264fee9fbece0eb5f89ead88630 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 14:26:45 +0900 Subject: [PATCH 053/164] =?UTF-8?q?test(auth):=20AuthServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - 회원가입_비밀번호_확인_불일치_실패 - 회원가입_성공 - 로그인_삭제된_유저의_이메일일_경우_실패 - 로그인_비밀번호가_일치하지_않을_경우_실패 - 로그인_성공 - 토큰_재발급_성공 --- .../dto/request/AuthSigninRequestDto.java | 2 + .../dto/request/AuthSignupRequestDto.java | 2 + .../domain/auth/service/AuthService.java | 2 +- .../domain/auth/service/AuthServiceTest.java | 144 ++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java index 3b08a29..3246310 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java @@ -1,8 +1,10 @@ package com.example.eightyage.domain.auth.dto.request; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class AuthSigninRequestDto { private String email; diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index 0defb39..482e7c0 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -3,9 +3,11 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class AuthSignupRequestDto { @NotBlank(message = "이메일은 필수 입력 값입니다.") diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 0340b6c..a9100fa 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -25,7 +25,7 @@ public class AuthService { public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { - throw new BadRequestException("비밀번호 확인을 입력해주세요"); + throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); } User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java new file mode 100644 index 0000000..1faab8c --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -0,0 +1,144 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.dto.request.AuthSigninRequestDto; +import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; +import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.UnauthorizedException; +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.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; + +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.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class AuthServiceTest { + + @Mock + private UserService userService; + @Mock + private TokenService tokenService; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private AuthService authService; + + @Test + void 회원가입_비밀번호_확인_불일치_실패() { + // given + AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341"); + + // when & then + assertThrows(BadRequestException.class, + () -> authService.signup(passwordCheckErrorSignupDto), + "비밀번호가 비밀번호 확인과 일치하지 않습니다."); + } + + @Test + void 회원가입_성공() { + // given + AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234"); + User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); + String accessToken = "accessToken"; + String refreshToken = "refreshToken"; + + given(userService.saveUser(any(String.class), any(String.class), any(String.class))).willReturn(user); + given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); + + // when + AuthTokensResponseDto result = authService.signup(successSignupDto); + + // then + assertEquals(accessToken, result.getAccessToken()); + assertEquals(refreshToken, result.getRefreshToken()); + } + + @Test + void 로그인_삭제된_유저의_이메일일_경우_실패() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", LocalDateTime.now()); + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + + // when & then + assertThrows(UnauthorizedException.class, + () -> authService.signin(seccessSigninDto), + "탈퇴한 유저 이메일입니다."); + } + + @Test + void 로그인_비밀번호가_일치하지_않을_경우_실패() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", null); + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(false); + + // when & then + assertThrows(UnauthorizedException.class, + () -> authService.signin(seccessSigninDto), + "잘못된 비밀번호입니다."); + } + + @Test + void 로그인_성공() { + // given + AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); + User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "deletedAt", null); + + String accessToken = "accessToken"; + String refreshToken = "refreshToken"; + + given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); + given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(true); + given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); + + // when + AuthTokensResponseDto result = authService.signin(seccessSigninDto); + + // then + assertEquals(accessToken, result.getAccessToken()); + assertEquals(refreshToken, result.getRefreshToken()); + } + + @Test + void 토큰_재발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refreshToken"; + + String reissuedAccessToken = "reissued-accessToken"; + String reissuedRefreshToken = "reissued-refreshToken"; + + given(tokenService.reissueToken(refreshToken)).willReturn(user); + given(tokenService.createAccessToken(any(User.class))).willReturn(reissuedAccessToken); + given(tokenService.createRefreshToken(any(User.class))).willReturn(reissuedRefreshToken); + + // when + AuthTokensResponseDto result = authService.reissueAccessToken(refreshToken); + + // then + assertEquals(reissuedAccessToken, result.getAccessToken()); + assertEquals(reissuedRefreshToken, result.getRefreshToken()); + } +} From d6e66f08db7bf8a9cd32da063fa6a7fafca8f256 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 14:43:26 +0900 Subject: [PATCH 054/164] =?UTF-8?q?test(auth):=20TokenServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - 토큰발급_AccessToken_발급_성공 - 토큰발급_RefreshToken_발급_성공 - 토큰유효성검사_성공 - 토큰유효성검사_비활성_상태일때_실패 - 토큰검색_토큰이_없을_시_실패 ### Test Coverage (line: 26%) --- .../domain/auth/service/TokenServiceTest.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java new file mode 100644 index 0000000..4ae6465 --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -0,0 +1,121 @@ +package com.example.eightyage.domain.auth.service; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import com.example.eightyage.domain.auth.repository.RefreshTokenRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.NotFoundException; +import com.example.eightyage.global.exception.UnauthorizedException; +import com.example.eightyage.global.util.JwtUtil; +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 java.util.Optional; + +import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TokenServiceTest { + + @Mock + private RefreshTokenRepository refreshTokenRepository; + @Mock + private UserService userService; + @Mock + private JwtUtil jwtUtil; + + @InjectMocks + private TokenService tokenService; + + /* createAccessToken */ + @Test + void 토큰발급_AccessToken_발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String accessToken = "accessToken"; + + given(jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole())).willReturn(accessToken); + + // when + String result = tokenService.createAccessToken(user); + + // then + assertEquals(accessToken, result); + } + + /* createRefreshToken */ + @Test + void 토큰발급_RefreshToken_발급_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + RefreshToken mockRefreshToken = new RefreshToken(user.getId()); + + given(refreshTokenRepository.save(any(RefreshToken.class))).willReturn(mockRefreshToken); + + // when + String createdRefreshToken = tokenService.createRefreshToken(user); + + // then + verify(refreshTokenRepository, times(1)).save(any(RefreshToken.class)); + assertEquals(mockRefreshToken.getToken(), createdRefreshToken); + } + + /* reissueToken */ + @Test + void 토큰유효성검사_비활성_상태일때_실패() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refresh-token"; + + RefreshToken mockRefreshToken = mock(RefreshToken.class); + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.of(mockRefreshToken)); + given(mockRefreshToken.getTokenState()).willReturn(INVALIDATED); + + // when & then + assertThrows(UnauthorizedException.class, + () -> tokenService.reissueToken(refreshToken), + "사용이 만료된 refresh token 입니다."); + } + + @Test + void 토큰검색_토큰이_없을_시_실패() { + //given + String refreshToken = "refresh-token"; + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> tokenService.reissueToken(refreshToken), + "리프레시 토큰을 찾을 수 없습니다."); + } + + @Test + void 토큰유효성검사_성공() { + // given + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + String refreshToken = "refresh-token"; + + RefreshToken mockRefreshToken = mock(RefreshToken.class); + + given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.of(mockRefreshToken)); + given(userService.findUserByIdOrElseThrow(mockRefreshToken.getUserId())).willReturn(user); + + // when + User result = tokenService.reissueToken(refreshToken); + + // then + assertNotNull(result); + verify(mockRefreshToken, times(1)).updateTokenStatus(INVALIDATED); + } +} From 14ea1257283519346fb586b058dd2082a7862e34 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:10:33 +0900 Subject: [PATCH 055/164] =?UTF-8?q?test(user):=20UserServiceTest=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 테스트 내용 - findById조회_userId가_없을_경우_실패 - findById조회_성공 - findByEmail조회_email이_없을_경우_실패 - findByEmail조회_성공 - 회원저장_중복된_이메일이_있을_경우_실패 - 회원저장_성공 - 회원탈퇴_회원이_존재하지_않으면_실패 - 회원탈퇴_비밀번호가_일치하지_않으면_실패 - 회원탈퇴_성공 ### Test Coverage (line: 38%) --- .../user/dto/request/UserDeleteRequest.java | 2 + .../domain/user/service/UserService.java | 2 +- .../domain/user/service/UserServiceTest.java | 197 ++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java index 06edae2..ef250c7 100644 --- a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java @@ -1,8 +1,10 @@ package com.example.eightyage.domain.user.dto.request; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class UserDeleteRequest { private String password; diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 6b07a80..0c86fb1 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -40,7 +40,7 @@ public void deleteUser(AuthUser authUser, UserDeleteRequest request) { User findUser = findUserByIdOrElseThrow(authUser.getUserId()); if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { - throw new BadRequestException("비밀번호가 일치하지 않습니다."); + throw new UnauthorizedException("비밀번호가 일치하지 않습니다."); } findUser.deleteUser(); diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..7e03f5f --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -0,0 +1,197 @@ +package com.example.eightyage.domain.user.service; + +import com.example.eightyage.domain.auth.entity.RefreshToken; +import com.example.eightyage.domain.auth.service.TokenService; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.NotFoundException; +import com.example.eightyage.global.exception.UnauthorizedException; +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.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserService userService; + + /* findUserByIdOrElseThrow */ + @Test + void findById조회_userId가_없을_경우_실패() { + // given + Long userId = 1L; + + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> userService.findUserByIdOrElseThrow(userId), + "해당 유저의 Id를 찾을 수 없습니다."); + } + + @Test + void findById조회_성공() { + // given + Long userId = 1L; + User user = new User(userId, "email@email.com", "nickname", UserRole.ROLE_USER); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + + // when + User resultUser = userService.findUserByIdOrElseThrow(userId); + + // then + assertNotNull(resultUser); + assertEquals(user.getId(), resultUser.getId()); + assertEquals(user.getEmail(), resultUser.getEmail()); + assertEquals(user.getNickname(), resultUser.getNickname()); + assertEquals(user.getUserRole(), resultUser.getUserRole()); + } + + /* findUserByEmailOrElseThrow */ + @Test + void findByEmail조회_email이_없을_경우_실패() { + // given + String email = "email@email.com"; + + given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty()); + + // when & then + assertThrows(UnauthorizedException.class, + () -> userService.findUserByEmailOrElseThrow(email), + "가입한 유저의 이메일이 아닙니다."); + } + + @Test + void findByEmail조회_성공() { + // given + String email = "email@email.com"; + User user = new User(1L, email, "nickname", UserRole.ROLE_USER); + + given(userRepository.findByEmail(any(String.class))).willReturn(Optional.of(user)); + + // when + User resultUser = userService.findUserByEmailOrElseThrow(email); + + // then + assertNotNull(resultUser); + assertEquals(user.getId(), resultUser.getId()); + assertEquals(user.getEmail(), resultUser.getEmail()); + assertEquals(user.getNickname(), resultUser.getNickname()); + assertEquals(user.getUserRole(), resultUser.getUserRole()); + } + + /* saveUser */ + @Test + void 회원저장_중복된_이메일이_있을_경우_실패() { + // given + String email = "email@email.com"; + String nickname = "nickname"; + String password = "password1234"; + + given(userRepository.existsByEmail(any(String.class))).willReturn(true); + + // when & then + assertThrows(BadRequestException.class, + () -> userService.saveUser(email, nickname, password), + "등록된 이메일입니다."); + } + + @Test + void 회원저장_성공() { + // given + String email = "email@email.com"; + String nickname = "nickname"; + String password = "password1234"; + User user = new User(email, nickname, password); + + String encodedPassword = "encoded-password1234"; + + given(userRepository.existsByEmail(any(String.class))).willReturn(false); + given(passwordEncoder.encode(any(String.class))).willReturn(encodedPassword); + given(userRepository.save(any(User.class))).willReturn(user); + + // when + User resultUser = userService.saveUser(email, nickname, password); + + // then + assertNotNull(resultUser); + assertEquals(email, resultUser.getEmail()); + assertEquals(nickname, resultUser.getNickname()); + assertEquals(password, resultUser.getPassword()); + } + + /* deleteUser */ + @Test + void 회원탈퇴_회원이_존재하지_않으면_실패() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); + + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when & then + assertThrows(NotFoundException.class, + () -> userService.deleteUser(authUser, successDeleteDto), + "해당 유저의 Id를 찾을 수 없습니다."); + } + + @Test + void 회원탈퇴_비밀번호가_일치하지_않으면_실패() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "password", "password1234"); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(false); + + // when & then + assertThrows(UnauthorizedException.class, + () -> userService.deleteUser(authUser, successDeleteDto), + "비밀번호가 일치하지 않습니다."); + } + + @Test + void 회원탈퇴_성공() { + // given + AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234"); + User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "password", "password1234"); + + given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); + given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(true); + + // when + userService.deleteUser(authUser, successDeleteDto); + + // then + assertNotNull(user.getDeletedAt()); + + } +} From ccac781229698e7ca32a3456a8b8235e2ee44218 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:25:32 +0900 Subject: [PATCH 056/164] =?UTF-8?q?fix(global):=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 에러코드를 출력할 수 있도록 @ResponseStatus 및 ResponseEntity 사용 --- .../global/exception/GlobalExceptionHandler.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index d297f34..79194a0 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -3,9 +3,11 @@ import com.example.eightyage.global.entity.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; @@ -15,11 +17,12 @@ public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) - public ErrorResponse invalidRequestExceptionException(CustomException ex) { + public ResponseEntity> invalidRequestExceptionException(CustomException ex) { HttpStatus httpStatus = ex.getHttpStatus(); - return ErrorResponse.of(httpStatus, ex.getMessage()); + return new ResponseEntity<>(ErrorResponse.of(httpStatus, ex.getMessage()), ex.getHttpStatus()); } + @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler public ErrorResponse> handleValidationException(MethodArgumentNotValidException e) { List fieldErrors = e.getBindingResult().getFieldErrors(); @@ -30,6 +33,7 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa return ErrorResponse.of(HttpStatus.BAD_REQUEST, validFailedList); } + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); From b9f7310bc134500a96e254c4402e0f39bb19fd75 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 16:37:44 +0900 Subject: [PATCH 057/164] =?UTF-8?q?fix(auth):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20ADMIN=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 관리자 테스트도 용이하게 만들기 위해 ADMIN을 작성하여 회원가입을 할수 있도록 수정 --- .../domain/auth/dto/request/AuthSignupRequestDto.java | 2 ++ .../eightyage/domain/auth/service/AuthService.java | 2 +- .../com/example/eightyage/domain/user/entity/User.java | 4 ++-- .../eightyage/domain/user/service/UserService.java | 5 +++-- .../eightyage/domain/auth/service/AuthServiceTest.java | 6 +++--- .../eightyage/domain/user/service/UserServiceTest.java | 8 +++++--- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index 482e7c0..c1687bf 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -23,4 +23,6 @@ public class AuthSignupRequestDto { private String password; private String passwordCheck; + + private String userRole; } diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index a9100fa..15c3a86 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -28,7 +28,7 @@ public AuthTokensResponseDto signup(AuthSignupRequestDto request) { throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); } - User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword()); + User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword(), request.getUserRole()); return getTokenResponse(user); } diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index 2809145..cfaa42f 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -27,11 +27,11 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String nickname, String password) { + public User(String email, String nickname, String password, UserRole userRole) { this.email = email; this.nickname = nickname; this.password = password; - this.userRole = UserRole.ROLE_USER; + this.userRole = userRole; } public User(Long id, String email, String nickname, UserRole userRole) { diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 0c86fb1..cc9364f 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -2,6 +2,7 @@ import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; @@ -21,7 +22,7 @@ public class UserService { /* 훠원저장 */ @Transactional - public User saveUser(String email, String nickname, String password) { + public User saveUser(String email, String nickname, String password, String userRole) { if (userRepository.existsByEmail(email)) { throw new BadRequestException("등록된 이메일입니다."); @@ -29,7 +30,7 @@ public User saveUser(String email, String nickname, String password) { String encodedPassword = passwordEncoder.encode(password); - User user = new User(email, nickname, encodedPassword); + User user = new User(email, nickname, encodedPassword, UserRole.of(userRole)); return userRepository.save(user); } diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java index 1faab8c..6843198 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -39,7 +39,7 @@ public class AuthServiceTest { @Test void 회원가입_비밀번호_확인_불일치_실패() { // given - AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341"); + AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341", "USER_ROLE"); // when & then assertThrows(BadRequestException.class, @@ -50,12 +50,12 @@ public class AuthServiceTest { @Test void 회원가입_성공() { // given - AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234"); + AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234", "USER_ROLE"); User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); String accessToken = "accessToken"; String refreshToken = "refreshToken"; - given(userService.saveUser(any(String.class), any(String.class), any(String.class))).willReturn(user); + given(userService.saveUser(any(String.class), any(String.class), any(String.class), any(String.class))).willReturn(user); given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java index 7e03f5f..0fa1b70 100644 --- a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -111,12 +111,13 @@ public class UserServiceTest { String email = "email@email.com"; String nickname = "nickname"; String password = "password1234"; + String userRole = "USER_ROLE"; given(userRepository.existsByEmail(any(String.class))).willReturn(true); // when & then assertThrows(BadRequestException.class, - () -> userService.saveUser(email, nickname, password), + () -> userService.saveUser(email, nickname, password, userRole), "등록된 이메일입니다."); } @@ -126,7 +127,8 @@ public class UserServiceTest { String email = "email@email.com"; String nickname = "nickname"; String password = "password1234"; - User user = new User(email, nickname, password); + String userRole = "ROLE_USER"; + User user = new User(email, nickname, password, UserRole.ROLE_USER); String encodedPassword = "encoded-password1234"; @@ -135,7 +137,7 @@ public class UserServiceTest { given(userRepository.save(any(User.class))).willReturn(user); // when - User resultUser = userService.saveUser(email, nickname, password); + User resultUser = userService.saveUser(email, nickname, password, userRole); // then assertNotNull(resultUser); From 660a1e001555c31891ebfa55aac4bb17a36b4c7d Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:49:50 +0900 Subject: [PATCH 058/164] =?UTF-8?q?refactor(auth):=20Builder=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 사용 --- .../dto/request/AuthSigninRequestDto.java | 2 ++ .../dto/request/AuthSignupRequestDto.java | 2 ++ .../response/AuthAccessTokenResponseDto.java | 2 ++ .../dto/response/AuthTokensResponseDto.java | 4 +++- .../domain/auth/entity/RefreshToken.java | 5 +++- ...Request.java => UserDeleteRequestDto.java} | 6 +++-- .../eightyage/domain/user/entity/User.java | 23 ++++++++++--------- .../eightyage/global/dto/AuthUser.java | 4 ++-- 8 files changed, 31 insertions(+), 17 deletions(-) rename src/main/java/com/example/eightyage/domain/user/dto/request/{UserDeleteRequest.java => UserDeleteRequestDto.java} (71%) diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java index 3246310..cfde537 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSigninRequestDto.java @@ -1,10 +1,12 @@ package com.example.eightyage.domain.auth.dto.request; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter @AllArgsConstructor +@Builder public class AuthSigninRequestDto { private String email; diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java index c1687bf..ad3680c 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/request/AuthSignupRequestDto.java @@ -4,9 +4,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthSignupRequestDto { diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java index 660a03d..bb1890d 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthAccessTokenResponseDto.java @@ -1,9 +1,11 @@ package com.example.eightyage.domain.auth.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthAccessTokenResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java index a7a77b3..6153686 100644 --- a/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/auth/dto/response/AuthTokensResponseDto.java @@ -1,13 +1,15 @@ package com.example.eightyage.domain.auth.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class AuthTokensResponseDto { - private final String AccessToken; + private final String accessToken; private final String refreshToken; } diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java index 0c1c3c1..0c98161 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java +++ b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java @@ -1,6 +1,8 @@ package com.example.eightyage.domain.auth.entity; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,7 +10,7 @@ @Entity @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class RefreshToken { @Id @@ -22,6 +24,7 @@ public class RefreshToken { @Enumerated(EnumType.STRING) private TokenState tokenState; + @Builder public RefreshToken(Long userId) { this.userId = userId; this.token = UUID.randomUUID().toString(); diff --git a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java similarity index 71% rename from src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java rename to src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java index ef250c7..3054ccd 100644 --- a/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequest.java +++ b/src/main/java/com/example/eightyage/domain/user/dto/request/UserDeleteRequestDto.java @@ -1,12 +1,14 @@ package com.example.eightyage.domain.user.dto.request; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor -public class UserDeleteRequest { +public class UserDeleteRequestDto { private String password; -} +} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index cfaa42f..f208901 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -3,6 +3,8 @@ import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,7 +12,7 @@ @Getter @Entity -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends TimeStamped { @Id @@ -27,23 +29,22 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String nickname, String password, UserRole userRole) { - this.email = email; - this.nickname = nickname; - this.password = password; - this.userRole = userRole; - } - - public User(Long id, String email, String nickname, UserRole userRole) { + @Builder + public User(Long id, String email, String nickname, String password, UserRole userRole) { this.id = id; this.email = email; this.nickname = nickname; + this.password = password; this.userRole = userRole; } public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getUserId(), authUser.getEmail(), authUser.getEmail(), - UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())); + return User.builder() + .id(authUser.getUserId()) + .email(authUser.getEmail()) + .nickname(authUser.getNickname()) + .userRole(UserRole.of(authUser.getAuthorities().iterator().next().getAuthority())) + .build(); } public void deleteUser() { diff --git a/src/main/java/com/example/eightyage/global/dto/AuthUser.java b/src/main/java/com/example/eightyage/global/dto/AuthUser.java index 8785cfe..022a0d4 100644 --- a/src/main/java/com/example/eightyage/global/dto/AuthUser.java +++ b/src/main/java/com/example/eightyage/global/dto/AuthUser.java @@ -1,7 +1,7 @@ package com.example.eightyage.global.dto; import com.example.eightyage.domain.user.entity.UserRole; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -10,7 +10,6 @@ import java.util.List; @Getter -@AllArgsConstructor public class AuthUser { private final Long userId; @@ -18,6 +17,7 @@ public class AuthUser { private final String nickname; private final Collection authorities; + @Builder public AuthUser(Long userId, String email, String nickname, UserRole role) { this.userId = userId; this.email = email; From f222d5d78faebf67e7a0c1f049e05e456d5fa82c Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:52:59 +0900 Subject: [PATCH 059/164] =?UTF-8?q?refactor(auth):=20Builder=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20enum=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage가 관리할 수 있도록 enum 자료형 작성 --- .../domain/auth/service/AuthService.java | 13 ++++--- .../domain/auth/service/TokenService.java | 6 ++-- .../user/controller/UserController.java | 9 ++--- .../domain/user/entity/UserRole.java | 4 ++- .../domain/user/service/UserService.java | 21 ++++++++---- .../argument/RefreshArgumentResolver.java | 7 ++-- .../eightyage/global/exception/ErrorCode.java | 10 +++--- .../global/exception/ErrorMessage.java | 34 +++++++++++++++++++ .../exception/GlobalExceptionHandler.java | 4 ++- .../filter/JwtAuthenticationFilter.java | 10 +++--- .../example/eightyage/global/util/.gitkeep | 0 .../eightyage/global/util/JwtUtil.java | 4 ++- 12 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/exception/ErrorMessage.java delete mode 100644 src/main/java/com/example/eightyage/global/util/.gitkeep diff --git a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java index 15c3a86..edf713a 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/AuthService.java @@ -12,6 +12,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Service @RequiredArgsConstructor public class AuthService { @@ -25,7 +27,7 @@ public class AuthService { public AuthTokensResponseDto signup(AuthSignupRequestDto request) { if (!request.getPassword().equals(request.getPasswordCheck())) { - throw new BadRequestException("비밀번호가 비밀번호 확인과 일치하지 않습니다."); + throw new BadRequestException(PASSWORD_CONFIRMATION_MISMATCH.getMessage()); } User user = userService.saveUser(request.getEmail(), request.getNickname(), request.getPassword(), request.getUserRole()); @@ -39,11 +41,11 @@ public AuthTokensResponseDto signin(AuthSigninRequestDto request) { User user = userService.findUserByEmailOrElseThrow(request.getEmail()); if (user.getDeletedAt() != null) { - throw new UnauthorizedException("탈퇴한 유저 이메일입니다."); + throw new UnauthorizedException(DEACTIVATED_USER_EMAIL.getMessage()); } if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - throw new UnauthorizedException("잘못된 비밀번호입니다."); + throw new UnauthorizedException(INVALID_PASSWORD.getMessage()); } return getTokenResponse(user); @@ -63,6 +65,9 @@ private AuthTokensResponseDto getTokenResponse(User user) { String accessToken = tokenService.createAccessToken(user); String refreshToken = tokenService.createRefreshToken(user); - return new AuthTokensResponseDto(accessToken, refreshToken); + return AuthTokensResponseDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); } } diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java index 6483524..b2c3514 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -11,6 +11,8 @@ import org.springframework.stereotype.Service; import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; @Service @RequiredArgsConstructor @@ -37,7 +39,7 @@ public User reissueToken(String token) { RefreshToken refreshToken = findByTokenOrElseThrow(token); if (refreshToken.getTokenState() == INVALIDATED) { - throw new UnauthorizedException("사용이 만료된 refresh token 입니다."); + throw new UnauthorizedException(EXPIRED_REFRESH_TOKEN.getMessage()); } refreshToken.updateTokenStatus(INVALIDATED); @@ -46,6 +48,6 @@ public User reissueToken(String token) { private RefreshToken findByTokenOrElseThrow(String token) { return refreshTokenRepository.findByToken(token).orElseThrow( - () -> new NotFoundException("리프레시 토큰을 찾을 수 없습니다.")); + () -> new NotFoundException(REFRESH_TOKEN_NOT_FOUND.getMessage())); } } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java index 21fce31..86adf60 100644 --- a/src/main/java/com/example/eightyage/domain/user/controller/UserController.java +++ b/src/main/java/com/example/eightyage/domain/user/controller/UserController.java @@ -1,13 +1,8 @@ package com.example.eightyage.domain.user.controller; -import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; -import com.example.eightyage.domain.auth.dto.response.AuthAccessTokenResponseDto; -import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.dto.AuthUser; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; @@ -26,7 +21,7 @@ public class UserController { @PostMapping("/v1/users/delete") public void signup( @AuthenticationPrincipal AuthUser authUser, - @RequestBody UserDeleteRequest request + @RequestBody UserDeleteRequestDto request ) { userService.deleteUser(authUser, request); } diff --git a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java index c384dd2..7f92a4f 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java @@ -6,6 +6,8 @@ import java.util.Arrays; +import static com.example.eightyage.global.exception.ErrorMessage.NOT_INVALID_USER_ROLE; + @Getter @RequiredArgsConstructor public enum UserRole { @@ -19,7 +21,7 @@ public static UserRole of(String role) { return Arrays.stream(UserRole.values()) .filter(r -> r.getUserRole().equalsIgnoreCase(role)) .findFirst() - .orElseThrow(() -> new UnauthorizedException("유효하지 않은 UserRole")); + .orElseThrow(() -> new UnauthorizedException(NOT_INVALID_USER_ROLE.getMessage())); } public static class Authority { diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index cc9364f..68c461b 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.user.service; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; @@ -13,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Service @RequiredArgsConstructor public class UserService { @@ -25,23 +27,28 @@ public class UserService { public User saveUser(String email, String nickname, String password, String userRole) { if (userRepository.existsByEmail(email)) { - throw new BadRequestException("등록된 이메일입니다."); + throw new BadRequestException(DUPLICATE_EMAIL.getMessage()); } String encodedPassword = passwordEncoder.encode(password); - User user = new User(email, nickname, encodedPassword, UserRole.of(userRole)); + User user = User.builder() + .email(email) + .nickname(nickname) + .password(encodedPassword) + .userRole(UserRole.of(userRole)) + .build(); return userRepository.save(user); } /* 회원탈퇴 */ @Transactional - public void deleteUser(AuthUser authUser, UserDeleteRequest request) { + public void deleteUser(AuthUser authUser, UserDeleteRequestDto request) { User findUser = findUserByIdOrElseThrow(authUser.getUserId()); if (!passwordEncoder.matches(request.getPassword(), findUser.getPassword())) { - throw new UnauthorizedException("비밀번호가 일치하지 않습니다."); + throw new UnauthorizedException(INVALID_PASSWORD.getMessage()); } findUser.deleteUser(); @@ -49,13 +56,13 @@ public void deleteUser(AuthUser authUser, UserDeleteRequest request) { public User findUserByEmailOrElseThrow(String email) { return userRepository.findByEmail(email).orElseThrow( - () -> new UnauthorizedException("가입한 유저의 이메일이 아닙니다.") + () -> new UnauthorizedException(USER_EMAIL_NOT_FOUND.getMessage()) ); } public User findUserByIdOrElseThrow(Long userId) { return userRepository.findById(userId).orElseThrow( - () -> new NotFoundException("해당 유저의 Id를 찾을 수 없습니다.") + () -> new NotFoundException(USER_ID_NOT_FOUND.getMessage()) ); } } diff --git a/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java index 5a5be23..c95cc79 100644 --- a/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java +++ b/src/main/java/com/example/eightyage/global/argument/RefreshArgumentResolver.java @@ -10,6 +10,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_MUST_BE_STRING; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; + public class RefreshArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { @@ -17,7 +20,7 @@ public boolean supportsParameter(MethodParameter parameter) { boolean isStringType = parameter.getParameterType().equals(String.class); if (hasRefreshTokenAnnotation != isStringType) { - throw new UnauthorizedException("@RefreshToken과 String 타입은 함께 사용되어야 합니다."); + throw new UnauthorizedException(REFRESH_TOKEN_MUST_BE_STRING.getMessage()); } return hasRefreshTokenAnnotation; } @@ -39,6 +42,6 @@ public Object resolveArgument( } } } - throw new UnauthorizedException("리프레시 토큰이 존재하지 않습니다. 다시 로그인 해주세요."); + throw new UnauthorizedException(REFRESH_TOKEN_NOT_FOUND.getMessage()); } } diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorCode.java b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java index 8063695..a9a2fc6 100644 --- a/src/main/java/com/example/eightyage/global/exception/ErrorCode.java +++ b/src/main/java/com/example/eightyage/global/exception/ErrorCode.java @@ -3,12 +3,14 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Getter public enum ErrorCode { - AUTHORIZATION(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), - BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), - NOT_FOUND(HttpStatus.NOT_FOUND, "찾지 못했습니다."), - FORBIDDEN(HttpStatus.FORBIDDEN, "권한이 없습니다."); + AUTHORIZATION(HttpStatus.UNAUTHORIZED, DEFAULT_UNAUTHORIZED.getMessage()), + BAD_REQUEST(HttpStatus.BAD_REQUEST, DEFAULT_BAD_REQUEST.getMessage()), + NOT_FOUND(HttpStatus.NOT_FOUND, DEFAULT_NOT_FOUND.getMessage()), + FORBIDDEN(HttpStatus.FORBIDDEN, DEFAULT_FORBIDDEN.getMessage()); private final HttpStatus status; private final String defaultMessage; diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java new file mode 100644 index 0000000..a3adf1d --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java @@ -0,0 +1,34 @@ +package com.example.eightyage.global.exception; + +import lombok.Getter; + +@Getter +public enum ErrorMessage { + NOT_INVALID_USER_ROLE("유효하지 않은 UserRole"), + NOT_FOUND_TOKEN("토큰을 찾을 수 없습니다."), + PASSWORD_CONFIRMATION_MISMATCH("비밀번호가 비밀번호 확인과 일치하지 않습니다."), + DEACTIVATED_USER_EMAIL("탈퇴한 유저 이메일입니다."), + INVALID_PASSWORD("비밀번호가 일치하지 않습니다."), + EXPIRED_REFRESH_TOKEN("사용이 만료된 refresh token 입니다."), + REFRESH_TOKEN_NOT_FOUND("리프레시 토큰을 찾을 수 없습니다."), + DUPLICATE_EMAIL("등록된 이메일입니다."), + USER_EMAIL_NOT_FOUND("가입한 유저의 이메일이 아닙니다."), + USER_ID_NOT_FOUND("해당 유저의 Id를 찾을 수 없습니다."), + REFRESH_TOKEN_MUST_BE_STRING("@RefreshToken과 String 타입은 함께 사용되어야 합니다."), + + DEFAULT_UNAUTHORIZED("인증이 필요합니다."), + DEFAULT_BAD_REQUEST("잘못된 요청입니다."), + DEFAULT_NOT_FOUND("찾지 못했습니다."), + DEFAULT_FORBIDDEN("권한이 없습니다."), + INTERNAL_SERVER_ERROR("서버 오류가 발생했습니다."), + + INVALID_JWT_SIGNATURE("유효하지 않는 JWT 서명입니다."), + EXPIRED_JWT_TOKEN("만료된 JWT 토큰입니다."), + UNSUPPORTED_JWT_TOKEN("지원되지 않는 JWT 토큰입니다."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 79194a0..650e95c 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -12,6 +12,8 @@ import java.util.List; +import static com.example.eightyage.global.exception.ErrorMessage.INTERNAL_SERVER_ERROR; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -37,6 +39,6 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); - return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."); + return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getMessage()); } } diff --git a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java index 3a8d43b..1e626ec 100644 --- a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java @@ -21,6 +21,8 @@ import java.io.IOException; +import static com.example.eightyage.global.exception.ErrorMessage.*; + @Slf4j @Component @RequiredArgsConstructor @@ -48,18 +50,18 @@ protected void doFilterInternal( } catch (SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, INVALID_JWT_SIGNATURE.getMessage()); return; } catch (ExpiredJwtException e) { log.error("Expired JWT token, 만료된 JWT token 입니다.", e); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, EXPIRED_JWT_TOKEN.getMessage()); return; } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, UNSUPPORTED_JWT_TOKEN.getMessage()); return; } catch (Exception e) { - log.error("Internal server error", e); + log.error(INTERNAL_SERVER_ERROR.getMessage(), e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } diff --git a/src/main/java/com/example/eightyage/global/util/.gitkeep b/src/main/java/com/example/eightyage/global/util/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/global/util/JwtUtil.java b/src/main/java/com/example/eightyage/global/util/JwtUtil.java index 9aea79f..8547e15 100644 --- a/src/main/java/com/example/eightyage/global/util/JwtUtil.java +++ b/src/main/java/com/example/eightyage/global/util/JwtUtil.java @@ -16,6 +16,8 @@ import java.util.Base64; import java.util.Date; +import static com.example.eightyage.global.exception.ErrorMessage.NOT_FOUND_TOKEN; + @Slf4j(topic = "JwtUtil") @Component public class JwtUtil { @@ -53,7 +55,7 @@ public String substringToken(String tokenValue) throws ServerException { if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { return tokenValue.substring(7); } - throw new ServerException("Not Found Token"); + throw new ServerException(NOT_FOUND_TOKEN.getMessage()); } public Claims extractClaims(String token) { From 16c2a8173cc821887b939356a919dc3b29234423 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 21:56:18 +0900 Subject: [PATCH 060/164] =?UTF-8?q?refactor(auth):=20AuthServiceTest=20Bui?= =?UTF-8?q?lder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/auth/service/AuthServiceTest.java | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java index 6843198..c65ea01 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -8,6 +8,7 @@ import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +19,7 @@ import java.time.LocalDateTime; +import static com.example.eightyage.global.exception.ErrorMessage.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -36,22 +38,55 @@ public class AuthServiceTest { @InjectMocks private AuthService authService; + private AuthSignupRequestDto successSignupDto; + private AuthSignupRequestDto passwordCheckErrorSignupDto; + private AuthSigninRequestDto successSigninDto; + private User user; + + @BeforeEach + public void setUp() { + passwordCheckErrorSignupDto = AuthSignupRequestDto.builder() + .email("email@email.com") + .nickname("nickname") + .password("password1234") + .passwordCheck("password1234!") + .userRole("USER_ROLE") + .build(); + + successSignupDto = AuthSignupRequestDto.builder() + .email("email@email.com") + .nickname("nickname") + .password("password1234") + .passwordCheck("password1234") + .userRole("USER_ROLE") + .build(); + + successSigninDto = AuthSigninRequestDto.builder() + .email("email@email.com") + .password("password1234") + .build(); + + user = User.builder() + .email(successSignupDto.getEmail()) + .nickname(successSignupDto.getNickname()) + .userRole(UserRole.ROLE_USER) + .build(); + + } + @Test void 회원가입_비밀번호_확인_불일치_실패() { // given - AuthSignupRequestDto passwordCheckErrorSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password12341", "USER_ROLE"); // when & then - assertThrows(BadRequestException.class, - () -> authService.signup(passwordCheckErrorSignupDto), - "비밀번호가 비밀번호 확인과 일치하지 않습니다."); + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> authService.signup(passwordCheckErrorSignupDto)); + assertEquals(badRequestException.getMessage(), PASSWORD_CONFIRMATION_MISMATCH.getMessage()); } @Test void 회원가입_성공() { // given - AuthSignupRequestDto successSignupDto = new AuthSignupRequestDto("email@email.com", "nickname", "password1234", "password1234", "USER_ROLE"); - User user = new User(1L, successSignupDto.getEmail(), successSignupDto.getNickname(), UserRole.ROLE_USER); String accessToken = "accessToken"; String refreshToken = "refreshToken"; @@ -70,51 +105,45 @@ public class AuthServiceTest { @Test void 로그인_삭제된_유저의_이메일일_경우_실패() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", LocalDateTime.now()); given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); // when & then - assertThrows(UnauthorizedException.class, - () -> authService.signin(seccessSigninDto), - "탈퇴한 유저 이메일입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> authService.signin(successSigninDto)); + assertEquals(unauthorizedException.getMessage(), DEACTIVATED_USER_EMAIL.getMessage()); } @Test void 로그인_비밀번호가_일치하지_않을_경우_실패() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", null); given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); - given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(false); + given(passwordEncoder.matches(successSigninDto.getPassword(), user.getPassword())).willReturn(false); // when & then - assertThrows(UnauthorizedException.class, - () -> authService.signin(seccessSigninDto), - "잘못된 비밀번호입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> authService.signin(successSigninDto)); + assertEquals(unauthorizedException.getMessage(), INVALID_PASSWORD.getMessage()); } @Test void 로그인_성공() { // given - AuthSigninRequestDto seccessSigninDto = new AuthSigninRequestDto("email@email.com", "password1234"); - User user = new User(1L, seccessSigninDto.getEmail(), "nickname", UserRole.ROLE_USER); ReflectionTestUtils.setField(user, "deletedAt", null); String accessToken = "accessToken"; String refreshToken = "refreshToken"; given(userService.findUserByEmailOrElseThrow(any(String.class))).willReturn(user); - given(passwordEncoder.matches(seccessSigninDto.getPassword(), user.getPassword())).willReturn(true); + given(passwordEncoder.matches(successSigninDto.getPassword(), user.getPassword())).willReturn(true); given(tokenService.createAccessToken(any(User.class))).willReturn(accessToken); given(tokenService.createRefreshToken(any(User.class))).willReturn(refreshToken); // when - AuthTokensResponseDto result = authService.signin(seccessSigninDto); + AuthTokensResponseDto result = authService.signin(successSigninDto); // then assertEquals(accessToken, result.getAccessToken()); @@ -124,7 +153,6 @@ public class AuthServiceTest { @Test void 토큰_재발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refreshToken"; String reissuedAccessToken = "reissued-accessToken"; From a35db2d25e925819a514e5d58f0bec77ddb893f1 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:02:35 +0900 Subject: [PATCH 061/164] =?UTF-8?q?refactor(auth):=20TokenServiceTest=20Bu?= =?UTF-8?q?ilder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/auth/service/TokenServiceTest.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java index 4ae6465..8e9492f 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -8,6 +8,7 @@ import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import com.example.eightyage.global.util.JwtUtil; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,6 +18,8 @@ import java.util.Optional; import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; +import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -36,11 +39,22 @@ public class TokenServiceTest { @InjectMocks private TokenService tokenService; + private User user; + + @BeforeEach + public void setUp() { + user = User.builder() + .email("email@email.com") + .nickname("nickname") + .userRole(UserRole.ROLE_USER) + .build(); + + } + /* createAccessToken */ @Test void 토큰발급_AccessToken_발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String accessToken = "accessToken"; given(jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getNickname(), user.getUserRole())).willReturn(accessToken); @@ -56,7 +70,6 @@ public class TokenServiceTest { @Test void 토큰발급_RefreshToken_발급_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); RefreshToken mockRefreshToken = new RefreshToken(user.getId()); given(refreshTokenRepository.save(any(RefreshToken.class))).willReturn(mockRefreshToken); @@ -73,7 +86,6 @@ public class TokenServiceTest { @Test void 토큰유효성검사_비활성_상태일때_실패() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refresh-token"; RefreshToken mockRefreshToken = mock(RefreshToken.class); @@ -82,9 +94,9 @@ public class TokenServiceTest { given(mockRefreshToken.getTokenState()).willReturn(INVALIDATED); // when & then - assertThrows(UnauthorizedException.class, - () -> tokenService.reissueToken(refreshToken), - "사용이 만료된 refresh token 입니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> tokenService.reissueToken(refreshToken)); + assertEquals(unauthorizedException.getMessage(), EXPIRED_REFRESH_TOKEN.getMessage()); } @Test @@ -95,15 +107,14 @@ public class TokenServiceTest { given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> tokenService.reissueToken(refreshToken), - "리프레시 토큰을 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> tokenService.reissueToken(refreshToken)); + assertEquals(notFoundException.getMessage(), REFRESH_TOKEN_NOT_FOUND.getMessage()); } @Test void 토큰유효성검사_성공() { // given - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); String refreshToken = "refresh-token"; RefreshToken mockRefreshToken = mock(RefreshToken.class); From ef9794c4e05c656cc6f24e180bde2bf8d0ab2198 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:22:49 +0900 Subject: [PATCH 062/164] =?UTF-8?q?refactor(user):=20UserServiceTest=20Bui?= =?UTF-8?q?lder=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 생성자의 파라미터 순서를 보장하기 위해 builder 적용 - 에러 메세지를 ErrorMessage로 관리하고 에러메세지 결과 비교 --- .../domain/user/service/UserServiceTest.java | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java index 0fa1b70..8041ceb 100644 --- a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -1,8 +1,6 @@ package com.example.eightyage.domain.user.service; -import com.example.eightyage.domain.auth.entity.RefreshToken; -import com.example.eightyage.domain.auth.service.TokenService; -import com.example.eightyage.domain.user.dto.request.UserDeleteRequest; +import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.entity.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; @@ -10,6 +8,7 @@ import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -20,12 +19,11 @@ import java.util.Optional; +import static com.example.eightyage.global.exception.ErrorMessage.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) public class UserServiceTest { @@ -38,6 +36,33 @@ public class UserServiceTest { @InjectMocks private UserService userService; + private User user; + private AuthUser authUser; + private UserDeleteRequestDto successDeleteDto; + private UserDeleteRequestDto wrongPasswordDeleteDto; + + @BeforeEach + public void setUp() { + user = User.builder() + .nickname("nickname") + .userRole(UserRole.ROLE_USER) + .build(); + + authUser = AuthUser.builder() + .userId(1L) + .email("email@email.com") + .nickname("nickname") + .role(UserRole.ROLE_USER) + .build(); + + successDeleteDto = UserDeleteRequestDto.builder() + .password("correct-password") + .build(); + wrongPasswordDeleteDto = UserDeleteRequestDto.builder() + .password("wrong-password") + .build(); + } + /* findUserByIdOrElseThrow */ @Test void findById조회_userId가_없을_경우_실패() { @@ -47,16 +72,16 @@ public class UserServiceTest { given(userRepository.findById(anyLong())).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> userService.findUserByIdOrElseThrow(userId), - "해당 유저의 Id를 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> userService.findUserByIdOrElseThrow(userId)); + assertEquals(notFoundException.getMessage(), USER_ID_NOT_FOUND.getMessage()); } @Test void findById조회_성공() { // given Long userId = 1L; - User user = new User(userId, "email@email.com", "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "id", userId); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); @@ -66,7 +91,6 @@ public class UserServiceTest { // then assertNotNull(resultUser); assertEquals(user.getId(), resultUser.getId()); - assertEquals(user.getEmail(), resultUser.getEmail()); assertEquals(user.getNickname(), resultUser.getNickname()); assertEquals(user.getUserRole(), resultUser.getUserRole()); } @@ -80,16 +104,16 @@ public class UserServiceTest { given(userRepository.findByEmail(any(String.class))).willReturn(Optional.empty()); // when & then - assertThrows(UnauthorizedException.class, - () -> userService.findUserByEmailOrElseThrow(email), - "가입한 유저의 이메일이 아닙니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> userService.findUserByEmailOrElseThrow(email)); + assertEquals(unauthorizedException.getMessage(), USER_EMAIL_NOT_FOUND.getMessage()); } @Test void findByEmail조회_성공() { // given String email = "email@email.com"; - User user = new User(1L, email, "nickname", UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "email", email); given(userRepository.findByEmail(any(String.class))).willReturn(Optional.of(user)); @@ -98,7 +122,6 @@ public class UserServiceTest { // then assertNotNull(resultUser); - assertEquals(user.getId(), resultUser.getId()); assertEquals(user.getEmail(), resultUser.getEmail()); assertEquals(user.getNickname(), resultUser.getNickname()); assertEquals(user.getUserRole(), resultUser.getUserRole()); @@ -116,9 +139,9 @@ public class UserServiceTest { given(userRepository.existsByEmail(any(String.class))).willReturn(true); // when & then - assertThrows(BadRequestException.class, - () -> userService.saveUser(email, nickname, password, userRole), - "등록된 이메일입니다."); + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> userService.saveUser(email, nickname, password, userRole)); + assertEquals(badRequestException.getMessage(), DUPLICATE_EMAIL.getMessage()); } @Test @@ -128,7 +151,9 @@ public class UserServiceTest { String nickname = "nickname"; String password = "password1234"; String userRole = "ROLE_USER"; - User user = new User(email, nickname, password, UserRole.ROLE_USER); + ReflectionTestUtils.setField(user, "email", "email@email.com"); + ReflectionTestUtils.setField(user, "password", "password1234"); + String encodedPassword = "encoded-password1234"; @@ -144,47 +169,40 @@ public class UserServiceTest { assertEquals(email, resultUser.getEmail()); assertEquals(nickname, resultUser.getNickname()); assertEquals(password, resultUser.getPassword()); + assertEquals(UserRole.of(userRole), resultUser.getUserRole()); + } /* deleteUser */ @Test void 회원탈퇴_회원이_존재하지_않으면_실패() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); - given(userRepository.findById(anyLong())).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, - () -> userService.deleteUser(authUser, successDeleteDto), - "해당 유저의 Id를 찾을 수 없습니다."); + NotFoundException notFoundException = assertThrows(NotFoundException.class, + () -> userService.deleteUser(authUser, successDeleteDto)); + assertEquals(notFoundException.getMessage(), USER_ID_NOT_FOUND.getMessage()); } @Test void 회원탈퇴_비밀번호가_일치하지_않으면_실패() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234!"); - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - ReflectionTestUtils.setField(user, "password", "password1234"); + ReflectionTestUtils.setField(user, "password", "correct-password"); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(false); // when & then - assertThrows(UnauthorizedException.class, - () -> userService.deleteUser(authUser, successDeleteDto), - "비밀번호가 일치하지 않습니다."); + UnauthorizedException unauthorizedException = assertThrows(UnauthorizedException.class, + () -> userService.deleteUser(authUser, wrongPasswordDeleteDto)); + assertEquals(unauthorizedException.getMessage(), INVALID_PASSWORD.getMessage()); } @Test void 회원탈퇴_성공() { // given - AuthUser authUser = new AuthUser(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - UserDeleteRequest successDeleteDto = new UserDeleteRequest("password1234"); - User user = new User(1L, "email@email.com", "nickname", UserRole.ROLE_USER); - ReflectionTestUtils.setField(user, "password", "password1234"); + ReflectionTestUtils.setField(user, "password", "wrong-password"); given(userRepository.findById(anyLong())).willReturn(Optional.of(user)); given(passwordEncoder.matches(any(String.class), any(String.class))).willReturn(true); @@ -194,6 +212,5 @@ public class UserServiceTest { // then assertNotNull(user.getDeletedAt()); - } } From ec1e183acca7e468191ce553e921fa6c788f9db3 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Tue, 25 Mar 2025 22:25:37 +0900 Subject: [PATCH 063/164] =?UTF-8?q?style(user):=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 점 - 회원수정 주석 수정 --- .../com/example/eightyage/domain/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index 68c461b..fc18d0c 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -22,7 +22,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - /* 훠원저장 */ + /* 회원저장 */ @Transactional public User saveUser(String email, String nickname, String password, String userRole) { From d12466fdb354de0815007c5e941b7c0f30625f63 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Wed, 26 Mar 2025 16:47:02 +0900 Subject: [PATCH 064/164] =?UTF-8?q?chore(search):=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 26 --------------- .../domain/search/entity/SearchLog.java | 1 + .../v1/controller/ProductControllerV1.java | 1 - .../v1/service/ProductServiceV1.java | 4 +-- .../v2/service/ProductServiceV2.java | 4 +-- .../domain/search/service/SearchService.java | 33 ------------------- .../search/v1/service/SearchServiceV1.java | 20 +++++++++++ ...earchService.java => SearchServiceV2.java} | 3 +- 8 files changed, 26 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/search/controller/SearchController.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/service/SearchService.java create mode 100644 src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java rename src/main/java/com/example/eightyage/domain/search/v2/service/{SearchService.java => SearchServiceV2.java} (98%) diff --git a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java deleted file mode 100644 index 1c31a3e..0000000 --- a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.eightyage.domain.search.controller; - -import com.example.eightyage.domain.search.dto.PopularKeywordDto; -import com.example.eightyage.domain.search.service.SearchService; -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 java.util.List; - -@RestController -@RequiredArgsConstructor -public class SearchController { - - private final SearchService searchService; - - @GetMapping("/api/v1/search/popular") - public ResponseEntity> searchPopularKeywords( - @RequestParam(defaultValue = "7") int days - ) { - return ResponseEntity.ok(searchService.searchPoplarKeywordsWithinDays(days)); - } - -} diff --git a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java index 89992cf..48588b4 100644 --- a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java +++ b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java @@ -7,6 +7,7 @@ import jakarta.persistence.Id; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java index 14a8295..0fdfd99 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; - @RestController @RequiredArgsConstructor public class ProductControllerV1 { diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java index 2f914fd..ad20ea0 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java @@ -4,7 +4,7 @@ import com.example.eightyage.domain.search.fakeProduct.entity.Category; import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; -import com.example.eightyage.domain.search.v2.service.SearchService; +import com.example.eightyage.domain.search.v1.service.SearchServiceV1; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -19,7 +19,7 @@ public class ProductServiceV1 { private final ProductRepository productRepository; - private final SearchService searchService; + private final SearchServiceV1 searchService; @Transactional(propagation = Propagation.REQUIRES_NEW) public Page getProducts(String productName, Category category, int size, int page) { diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java index a198177..5627f88 100644 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java @@ -4,7 +4,7 @@ import com.example.eightyage.domain.search.fakeProduct.entity.Category; import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; -import com.example.eightyage.domain.search.v2.service.SearchService; +import com.example.eightyage.domain.search.v2.service.SearchServiceV2; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -19,7 +19,7 @@ public class ProductServiceV2 { private final ProductRepository productRepository; - private final SearchService searchService; + private final SearchServiceV2 searchService; @Transactional(propagation = Propagation.REQUIRES_NEW) public Page getProducts(String productName, Category category, int size, int page) { diff --git a/src/main/java/com/example/eightyage/domain/search/service/SearchService.java b/src/main/java/com/example/eightyage/domain/search/service/SearchService.java deleted file mode 100644 index ff5b772..0000000 --- a/src/main/java/com/example/eightyage/domain/search/service/SearchService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.eightyage.domain.search.service; - -import com.example.eightyage.domain.search.dto.PopularKeywordDto; -import com.example.eightyage.domain.search.entity.SearchLog; -import com.example.eightyage.domain.search.repository.SearchLogRepository; -import com.example.eightyage.global.exception.BadRequestException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class SearchService { - - private final SearchLogRepository searchLogRepository; - - public void saveKeyword(String keyword){ - if(StringUtils.hasText(keyword)){ - searchLogRepository.save(SearchLog.of(keyword)); - } - } - - public List searchPoplarKeywordsWithinDays(int days) { - if(days<0 || days >365){ - throw new BadRequestException("조회 기간은 1 ~ 365일 사이여야 합니다."); - } - LocalDateTime since = LocalDateTime.now().minusDays(days); - return searchLogRepository.findPopularKeywords(since); - } -} diff --git a/src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java b/src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java new file mode 100644 index 0000000..436b145 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java @@ -0,0 +1,20 @@ +package com.example.eightyage.domain.search.v1.service; + +import com.example.eightyage.domain.search.entity.SearchLog; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Service +@RequiredArgsConstructor +public class SearchServiceV1 { + + private final SearchLogRepository searchLogRepository; + + public void saveSearchLog(String keyword){ + if(StringUtils.hasText(keyword)){ + searchLogRepository.save(SearchLog.of(keyword)); + } + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java b/src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java similarity index 98% rename from src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java rename to src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java index 6cbad9c..00e0ebf 100644 --- a/src/main/java/com/example/eightyage/domain/search/v2/service/SearchService.java +++ b/src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java @@ -12,10 +12,9 @@ import java.util.HashSet; import java.util.Set; - @Service @RequiredArgsConstructor -public class SearchService { +public class SearchServiceV2 { private final SearchLogRepository searchLogRepository; private final CacheManager cacheManager; From 23776c6bbbd485661faef55abbfb6d136c658648 Mon Sep 17 00:00:00 2001 From: peridot Date: Wed, 26 Mar 2025 16:57:24 +0900 Subject: [PATCH 065/164] =?UTF-8?q?feat(coupon):=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=9E=84=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/controller/.gitkeep | 0 .../coupon/controller/CouponController.java | 26 ++++++++++++++++ .../domain/coupon/dto/response/.gitkeep | 0 .../dto/response/CouponResponseDto.java | 29 +++++++++++++++++ .../domain/coupon/entity/Coupon.java | 3 +- .../domain/coupon/repository/.gitkeep | 0 .../coupon/repository/CouponRepository.java | 9 ++++++ .../domain/event/service/CouponService.java | 31 +++++++++++++++++++ .../global/util/RandomCodeGenerator.java | 18 +++++++++++ 9 files changed, 115 insertions(+), 1 deletion(-) delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/controller/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/response/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/repository/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/event/service/CouponService.java create mode 100644 src/main/java/com/example/eightyage/global/util/RandomCodeGenerator.java diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java new file mode 100644 index 0000000..f2a1cfb --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -0,0 +1,26 @@ +package com.example.eightyage.domain.coupon.controller; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.event.service.CouponService; +import com.example.eightyage.global.dto.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("api/v1/coupon") +@RequiredArgsConstructor +public class CouponController { + + private final CouponService couponService; + + @PostMapping("/{eventId}") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable String eventId) { + return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); + } + +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/dto/response/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java new file mode 100644 index 0000000..d1eca80 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.coupon.dto.response; + +import com.example.eightyage.domain.coupon.entity.CouponState; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class CouponResponseDto { + + private final String couponCode; + private final CouponState state; + private final String username; + private final String eventname; + + private final LocalDateTime startAt; + private final LocalDateTime endAt; + + public CouponResponseDto(String couponCode, CouponState state, + String username, String eventname, + LocalDateTime startAt, LocalDateTime endAt) { + this.couponCode = couponCode; + this.state = state; + this.username = username; + this.eventname = eventname; + this.startAt = startAt; + this.endAt = endAt; + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java index 2b488f1..9a2affb 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -16,7 +16,8 @@ public class Coupon extends TimeStamped { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String coupon_number; + @Column(unique = true) + private String couponCode; @Enumerated(EnumType.STRING) private CouponState state; diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java new file mode 100644 index 0000000..d617d00 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java @@ -0,0 +1,9 @@ +package com.example.eightyage.domain.coupon.repository; + +import com.example.eightyage.domain.coupon.entity.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CouponRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/eightyage/domain/event/service/CouponService.java b/src/main/java/com/example/eightyage/domain/event/service/CouponService.java new file mode 100644 index 0000000..be0f3b8 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/event/service/CouponService.java @@ -0,0 +1,31 @@ +package com.example.eightyage.domain.event.service; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.repository.CouponRepository; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.util.RandomCodeGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CouponService { + + private final CouponRepository couponRepository; + private final RandomCodeGenerator randomCodeGenerator; + + public CouponResponseDto issueCoupon(AuthUser authUser, String eventId) { + // 해당 이벤트 조회 + + // 이벤트 상태 및 수량 확인 + + // 사용자 발급 여부 확인 + + // 랜덤 쿠폰 번호 생성 + String couponCode = randomCodeGenerator.generateCouponCode(10); + // 쿠폰 발급 (DB 저장) + + // 응답: 발급된 쿠폰 정보 + return new CouponResponseDto(); + } +} diff --git a/src/main/java/com/example/eightyage/global/util/RandomCodeGenerator.java b/src/main/java/com/example/eightyage/global/util/RandomCodeGenerator.java new file mode 100644 index 0000000..dd8f121 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/util/RandomCodeGenerator.java @@ -0,0 +1,18 @@ +package com.example.eightyage.global.util; + +import java.security.SecureRandom; + +public class RandomCodeGenerator { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final SecureRandom random = new SecureRandom(); + + public static String generateCouponCode(int length) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length()))); + } + return sb.toString(); + } +} + From 5a4eec2adfc88632f9ccefbed401842c55f5cf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 26 Mar 2025 17:29:07 +0900 Subject: [PATCH 066/164] =?UTF-8?q?feat:=20=EC=A0=9C=ED=92=88=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20#15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../controller/ProductImageController.java | 37 ++++++++++ .../domain/product/entity/ProductImage.java | 28 ++++++++ .../repository/ProductImageRepository.java | 19 ++++++ .../product/service/ProductImageService.java | 68 +++++++++++++++++++ .../eightyage/global/config/S3Config.java | 33 +++++++++ 6 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java create mode 100644 src/main/java/com/example/eightyage/domain/product/entity/ProductImage.java create mode 100644 src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java create mode 100644 src/main/java/com/example/eightyage/global/config/S3Config.java diff --git a/build.gradle b/build.gradle index 1524b0a..f55cac0 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' // spring cloud AWS S3 -// implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java new file mode 100644 index 0000000..72b0cfa --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java @@ -0,0 +1,37 @@ +package com.example.eightyage.domain.product.controller; + +import com.example.eightyage.domain.product.service.ProductImageService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; + +@RestController +@RequestMapping("/api/v1/products") +@RequiredArgsConstructor +public class ProductImageController { + + private final ProductImageService productImageService; + + // 제품 이미지 업로드 + @Secured("ROLE_ADMIN") + @PostMapping("/{productId}/images") + public ResponseEntity uploadImage( + @PathVariable Long productId, + @RequestParam("file") MultipartFile file) throws IOException { + + String imageUrl = productImageService.uploadImage(productId, file); + return ResponseEntity.ok(imageUrl); + } + + // 제품 이미지 삭제 + @Secured("ROLE_ADMIN") + @DeleteMapping("/images/{imageId}") + public ResponseEntity deleteImage(@PathVariable Long imageId) { + productImageService.deleteImage(imageId); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/entity/ProductImage.java b/src/main/java/com/example/eightyage/domain/product/entity/ProductImage.java new file mode 100644 index 0000000..2973890 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/entity/ProductImage.java @@ -0,0 +1,28 @@ +package com.example.eightyage.domain.product.entity; + +import com.example.eightyage.global.entity.TimeStamped; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "product_image") +public class ProductImage extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + private String imageUrl; + + public ProductImage(Product product, String imageUrl) { + this.product = product; + this.imageUrl = imageUrl; + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java new file mode 100644 index 0000000..1d47f09 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java @@ -0,0 +1,19 @@ +package com.example.eightyage.domain.product.repository; + +import com.example.eightyage.domain.product.entity.ProductImage; +import com.example.eightyage.global.exception.NotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.awt.*; +import java.util.Optional; + +public interface ProductImageRepository extends JpaRepository { + + @Query("SELECT pi FROM ProductImage pi WHERE pi.id = :imageId AND pi.deletedAt IS NULL") + Optional findById(Long imageId); + + default ProductImage findProductImageByIdOrElseThrow(Long imageId){ + return findById(imageId).orElseThrow(() -> new NotFoundException("해당 이미지가 존재하지 않습니다.")); + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java new file mode 100644 index 0000000..e79f528 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -0,0 +1,68 @@ +package com.example.eightyage.domain.product.service; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.ProductImage; +import com.example.eightyage.domain.product.repository.ProductImageRepository; +import com.example.eightyage.domain.product.repository.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ProductImageService { + + private final S3Client s3Client; + private final ProductImageRepository productImageRepository; + private final ProductRepository productRepository; + + @Value("${aws.s3.bucket}") + private String bucket; + + @Value("${aws.region}") + private String region; + + // 제품 이미지 업로드 + @Transactional + public String uploadImage(Long productId, MultipartFile file) throws IOException { + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); // 파일명 중복 방지 + + // S3에 업로드 + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + + // S3 이미지 URL 생성 + String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, fileName); + + // DB 저장 + Product product = productRepository.findProductByIdOrElseThrow(productId); + ProductImage productImage = new ProductImage(product, imageUrl); + productImageRepository.save(productImage); + + return imageUrl; + } + + // 제품 이미지 삭제 + @Transactional + public void deleteImage(Long imageId) { + ProductImage findProductImage = productImageRepository.findProductImageByIdOrElseThrow(imageId); + + findProductImage.setDeletedAt(LocalDateTime.now()); + } +} + diff --git a/src/main/java/com/example/eightyage/global/config/S3Config.java b/src/main/java/com/example/eightyage/global/config/S3Config.java new file mode 100644 index 0000000..b62e367 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/S3Config.java @@ -0,0 +1,33 @@ +package com.example.eightyage.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Value("${aws.region}") + private String region; + + @Value("${aws.credentials.access-key}") + private String accessKey; + + @Value("${aws.credentials.secret-key}") + private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey) + )) + .build(); + } +} + From a8ba9b89e923d0a2e95caf8fb4c7746e9916a300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 26 Mar 2025 19:12:38 +0900 Subject: [PATCH 067/164] =?UTF-8?q?fix:=20Product=20validation=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 2 +- .../domain/product/dto/request/ProductSaveRequestDto.java | 5 +---- .../product/dto/request/ProductUpdateRequestDto.java | 7 ------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 19b88a1..607f602 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -34,7 +34,7 @@ public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDt @PatchMapping("/{productId}") public ResponseEntity updateProduct( @PathVariable Long productId, - @Valid @RequestBody ProductUpdateRequestDto requestDto + @RequestBody ProductUpdateRequestDto requestDto ){ ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getSaleState(), requestDto.getPrice()); diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java index bcb56f1..78ed8ee 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java @@ -5,16 +5,13 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; -import java.util.HashSet; -import java.util.Set; - @Getter public class ProductSaveRequestDto { @NotBlank(message="반드시 값이 있어야 합니다.") private String productName; - @NotBlank(message="반드시 값이 있어야 합니다.") + @NotNull(message="반드시 값이 있어야 합니다.") private Category category; @NotBlank(message="반드시 값이 있어야 합니다.") diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java index 978880d..5b0e3ab 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -2,25 +2,18 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Getter; @Getter public class ProductUpdateRequestDto { - @NotBlank(message="반드시 값이 있어야 합니다.") private String productName; - @NotBlank(message="반드시 값이 있어야 합니다.") private Category category; - @NotBlank(message="반드시 값이 있어야 합니다.") private String content; - @NotBlank(message="반드시 값이 있어야 합니다.") private SaleState saleState; - @NotNull(message="반드시 값이 있어야 합니다.") private Integer price; } From 38162c27ef839984caf90187eb6eb8fbd4981725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 26 Mar 2025 19:12:57 +0900 Subject: [PATCH 068/164] =?UTF-8?q?fix:=20Review=20validation=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/controller/ReviewController.java | 2 +- .../domain/review/dto/request/ReviewUpdateRequestDto.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index 335993a..acf09c0 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -46,7 +46,7 @@ public ResponseEntity saveReview( public ResponseEntity updateReview( @AuthenticationPrincipal AuthUser authUser, @PathVariable Long reviewId, - @Valid @RequestBody ReviewUpdateRequestDto requestDto + @RequestBody ReviewUpdateRequestDto requestDto ){ ReviewUpdateResponseDto responseDto = reviewService.updateReview(authUser.getUserId(), reviewId, requestDto.getScore(), requestDto.getContent()); diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java index f6f0f0b..c6b3e36 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -6,9 +6,7 @@ @Getter public class ReviewUpdateRequestDto { - @NotNull(message = "반드시 값이 있어야 합니다.") private Double score; - @NotBlank(message = "반드시 값이 있어야 합니다.") private String content; } \ No newline at end of file From 3fabaec0dca3fdc3df9c88e893b9652040efcf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 26 Mar 2025 20:20:14 +0900 Subject: [PATCH 069/164] =?UTF-8?q?chore:=20ci.yml=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B0=8D=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19cb1c2..6a531fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,10 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: Test And Build with Gradle + - name: Clean Gradle Cache (Optional but good) + run: ./gradlew clean --refresh-dependencies + - name: Test And Build with Gradle (Debug Mode) env: JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - run: ./gradlew clean build -Dspring.profiles.active=ci \ No newline at end of file + run: ./gradlew build --stacktrace --info -Dspring.profiles.active=ci + From 23a4aaeda96f6be20d61654b0e07e6fc4bcf2ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 26 Mar 2025 20:31:05 +0900 Subject: [PATCH 070/164] =?UTF-8?q?refactor:=20@Setter=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=ED=9B=84=20setDeletedAt=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/eightyage/global/entity/TimeStamped.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index e91041c..1a49cc9 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -24,8 +24,11 @@ public abstract class TimeStamped { @Temporal(TemporalType.TIMESTAMP) private LocalDateTime modifiedAt; - @Setter @Column @Temporal(TemporalType.TIMESTAMP) - @Setter private LocalDateTime deletedAt; + private LocalDateTime deletedAt; + + public void setDeletedAt(LocalDateTime deletedAt) { + this.deletedAt = deletedAt; + } } From 1c100651bb6b2b76ac89e0e9b54b14cf491e0146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 10:23:35 +0900 Subject: [PATCH 071/164] =?UTF-8?q?refactor:=20RequestMapping=20URL=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 10 +++++----- .../product/controller/ProductImageController.java | 6 +++--- .../domain/review/controller/ReviewController.java | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 607f602..75d7254 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/products") +@RequestMapping("/api") @RequiredArgsConstructor public class ProductController { @@ -22,7 +22,7 @@ public class ProductController { // 제품 생성 @Secured("ROLE_ADMIN") - @PostMapping + @PostMapping("/v1/products") public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDto requestDto){ productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); @@ -31,7 +31,7 @@ public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDt // 제품 수정 @Secured("ROLE_ADMIN") - @PatchMapping("/{productId}") + @PatchMapping("/v1/products/{productId}") public ResponseEntity updateProduct( @PathVariable Long productId, @RequestBody ProductUpdateRequestDto requestDto @@ -42,7 +42,7 @@ public ResponseEntity updateProduct( } // 제품 단건 조회 - @GetMapping("/{productId}") + @GetMapping("/v1/products/{productId}") public ResponseEntity getProduct(@PathVariable Long productId){ ProductGetResponseDto responseDto = productService.findProductById(productId); @@ -51,7 +51,7 @@ public ResponseEntity getProduct(@PathVariable Long produ // 제품 삭제 @Secured("ROLE_ADMIN") - @DeleteMapping("/{productId}") + @DeleteMapping("/v1/products/{productId}") public ResponseEntity deleteProduct(@PathVariable Long productId){ productService.deleteProduct(productId); diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java index 72b0cfa..37ff8c0 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java @@ -10,7 +10,7 @@ import java.io.IOException; @RestController -@RequestMapping("/api/v1/products") +@RequestMapping("/api") @RequiredArgsConstructor public class ProductImageController { @@ -18,7 +18,7 @@ public class ProductImageController { // 제품 이미지 업로드 @Secured("ROLE_ADMIN") - @PostMapping("/{productId}/images") + @PostMapping("/v1/products/{productId}/images") public ResponseEntity uploadImage( @PathVariable Long productId, @RequestParam("file") MultipartFile file) throws IOException { @@ -29,7 +29,7 @@ public ResponseEntity uploadImage( // 제품 이미지 삭제 @Secured("ROLE_ADMIN") - @DeleteMapping("/images/{imageId}") + @DeleteMapping("/v1/products/images/{imageId}") public ResponseEntity deleteImage(@PathVariable Long imageId) { productImageService.deleteImage(imageId); return new ResponseEntity<>(HttpStatus.OK); diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index acf09c0..de99c36 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1") +@RequestMapping("/api") @RequiredArgsConstructor public class ReviewController { @@ -29,7 +29,7 @@ public class ReviewController { // 리뷰 생성 @Secured("ROLE_USER") - @PostMapping("/products/{productId}/reviews") + @PostMapping("/v1/products/{productId}/reviews") public ResponseEntity saveReview( @AuthenticationPrincipal AuthUser authUser, @PathVariable Long productId, @@ -42,7 +42,7 @@ public ResponseEntity saveReview( // 리뷰 수정 @Secured("ROLE_USER") - @PatchMapping("/reviews/{reviewId}") + @PatchMapping("/v1/reviews/{reviewId}") public ResponseEntity updateReview( @AuthenticationPrincipal AuthUser authUser, @PathVariable Long reviewId, @@ -54,7 +54,7 @@ public ResponseEntity updateReview( } // 리뷰 다건 조회 - @GetMapping("/products/{productId}/reviews") + @GetMapping("/v1/products/{productId}/reviews") public ResponseEntity> getReviews( @PathVariable Long productId, @RequestParam(required = false, defaultValue = "score") String orderBy, @@ -73,7 +73,7 @@ public ResponseEntity> getReviews( // 리뷰 삭제 @Secured("ROLE_USER") - @DeleteMapping("/reviews/{reviewId}") + @DeleteMapping("/v1/reviews/{reviewId}") public ResponseEntity deleteReview( @AuthenticationPrincipal AuthUser authUser, @PathVariable Long reviewId From 575c1b12ae402934918f20ad4be51ffc51e8947c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 10:31:15 +0900 Subject: [PATCH 072/164] =?UTF-8?q?refactor:=20saveProduct=20=EB=A6=AC?= =?UTF-8?q?=ED=84=B4=20=ED=83=80=EC=9E=85=EC=9D=84=20void=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20responseDto=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 6 +++--- .../eightyage/domain/product/service/ProductService.java | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 75d7254..608aa26 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -23,10 +23,10 @@ public class ProductController { // 제품 생성 @Secured("ROLE_ADMIN") @PostMapping("/v1/products") - public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDto requestDto){ - productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); + public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDto requestDto){ + ProductSaveResponseDto responseDto = productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); - return new ResponseEntity<>(HttpStatus.CREATED); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } // 제품 수정 diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 2620ad1..35caecd 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -29,10 +29,12 @@ public class ProductService { // 제품 생성 @Transactional - public void saveProduct(String productName, Category category, String content, Integer price) { + public ProductSaveResponseDto saveProduct(String productName, Category category, String content, Integer price) { Product product = new Product(productName, category, content, price, SaleState.FOR_SALE); - productRepository.save(product); + Product savedProduct = productRepository.save(product); + + return new ProductSaveResponseDto(savedProduct.getName(), savedProduct.getCategory(), savedProduct.getPrice(), savedProduct.getSaleState(), savedProduct.getCreatedAt(), savedProduct.getModifiedAt()); } // 제품 수정 From 7542a5a7bff49765c0c0ec74478bc6db182bf43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 10:35:58 +0900 Subject: [PATCH 073/164] =?UTF-8?q?refactor:=20DELETE=20API=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EA=B0=92=20OK=20(=20200=20)=20=EC=97=90=EC=84=9C=20N?= =?UTF-8?q?=5FCONTENT=20(=20204=20)=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/controller/ProductController.java | 2 +- .../domain/product/controller/ProductImageController.java | 2 +- .../eightyage/domain/review/controller/ReviewController.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 608aa26..a79d19b 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -55,6 +55,6 @@ public ResponseEntity getProduct(@PathVariable Long produ public ResponseEntity deleteProduct(@PathVariable Long productId){ productService.deleteProduct(productId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java index 37ff8c0..4055410 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java @@ -32,6 +32,6 @@ public ResponseEntity uploadImage( @DeleteMapping("/v1/products/images/{imageId}") public ResponseEntity deleteImage(@PathVariable Long imageId) { productImageService.deleteImage(imageId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index de99c36..c04c09e 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -80,6 +80,6 @@ public ResponseEntity deleteReview( ){ reviewService.deleteReview(authUser.getUserId(), reviewId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } From 0a2e54f3036ee21ce7fb767b403d1d246f646fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 10:42:17 +0900 Subject: [PATCH 074/164] =?UTF-8?q?refactor:=20ResponseEntity=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=A9=EC=8B=9D=20=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 4 ++-- .../eightyage/domain/review/controller/ReviewController.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index a79d19b..bcf8666 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -38,7 +38,7 @@ public ResponseEntity updateProduct( ){ ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getSaleState(), requestDto.getPrice()); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return ResponseEntity.ok(responseDto); } // 제품 단건 조회 @@ -46,7 +46,7 @@ public ResponseEntity updateProduct( public ResponseEntity getProduct(@PathVariable Long productId){ ProductGetResponseDto responseDto = productService.findProductById(productId); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return ResponseEntity.ok(responseDto); } // 제품 삭제 diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index c04c09e..d1c4539 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -50,7 +50,7 @@ public ResponseEntity updateReview( ){ ReviewUpdateResponseDto responseDto = reviewService.updateReview(authUser.getUserId(), reviewId, requestDto.getScore(), requestDto.getContent()); - return new ResponseEntity<>(responseDto, HttpStatus.OK); + return ResponseEntity.ok(responseDto); } // 리뷰 다건 조회 @@ -68,7 +68,7 @@ public ResponseEntity> getReviews( Page reviews = reviewService.findReviews(productId, sortedPageable); - return new ResponseEntity<>(reviews, HttpStatus.OK); + return ResponseEntity.ok(reviews); } // 리뷰 삭제 From 3e4da7aacbc808ed393dadbd768c7246a24a3b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 10:52:58 +0900 Subject: [PATCH 075/164] =?UTF-8?q?refactor:=20Validation=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EB=A5=BC=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/request/ProductSaveRequestDto.java | 9 +++++---- .../domain/review/dto/request/ReviewSaveRequestDto.java | 5 +++-- .../example/eightyage/global/dto/ValidationMessage.java | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java index 78ed8ee..c8d2a04 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java @@ -1,6 +1,7 @@ package com.example.eightyage.domain.product.dto.request; import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -8,15 +9,15 @@ @Getter public class ProductSaveRequestDto { - @NotBlank(message="반드시 값이 있어야 합니다.") + @NotBlank(message= ValidationMessage.NOT_BLANK_PRODUCT_NAME) private String productName; - @NotNull(message="반드시 값이 있어야 합니다.") + @NotNull(message=ValidationMessage.NOT_NULL_CATEGORY) private Category category; - @NotBlank(message="반드시 값이 있어야 합니다.") + @NotBlank(message=ValidationMessage.NOT_BLANK_CONTENT) private String content; - @NotNull(message="반드시 값이 있어야 합니다.") + @NotNull(message=ValidationMessage.NOT_NULL_PRICE) private Integer price; } diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java index 4a5e3d4..9c1d183 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.review.dto.request; +import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -7,9 +8,9 @@ @Getter public class ReviewSaveRequestDto { - @NotNull(message = "반드시 값이 있어야 합니다.") + @NotNull(message = ValidationMessage.NOT_NULL_SCORE) private Double score; - @NotBlank(message = "반드시 값이 있어야 합니다.") + @NotBlank(message = ValidationMessage.NOT_BLANK_CONTENT) private String content; } diff --git a/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java index ea339d6..d7edd98 100644 --- a/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java +++ b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java @@ -10,6 +10,11 @@ public final class ValidationMessage { public static final String PATTERN_EMAIL = "이메일 형식으로 입력되어야 합니다."; public static final String NOT_BLANK_NICKNAME = "닉네임은 필수 입력 값입니다."; public static final String NOT_BLANK_PASSWORD = "비밀번호는 필수 입력 값입니다."; + public static final String NOT_NULL_SCORE = "별점은 필수 입력 값입니다."; + public static final String NOT_BLANK_CONTENT = "컨텐트는 필수 입력 값입니다."; + public static final String NOT_BLANK_PRODUCT_NAME = "상품명은 필수 입력 값입니다."; + public static final String NOT_NULL_CATEGORY = "카테고리는 필수 입력 값입니다."; + public static final String NOT_NULL_PRICE = "가격은 필수 입력 값입니다."; public static final String PATTERN_PASSWORD = "비밀번호는 영어, 숫자 포함 8자리 이상이어야 합니다."; public static final String PATTERN_PASSWORD_REGEXP = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"; From e01e2bc91beb7eeffc165f47eaccc0237a7fff09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 11:14:16 +0900 Subject: [PATCH 076/164] =?UTF-8?q?fix:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=82=B4=EC=97=90=EC=84=9C=20IOException=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=ED=9B=84=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=98=EC=A7=80=EB=8A=94=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ProductImageController.java | 2 +- .../product/service/ProductImageService.java | 41 +++++++++++-------- .../exception/GlobalExceptionHandler.java | 6 +++ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java index 4055410..165a743 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductImageController.java @@ -21,7 +21,7 @@ public class ProductImageController { @PostMapping("/v1/products/{productId}/images") public ResponseEntity uploadImage( @PathVariable Long productId, - @RequestParam("file") MultipartFile file) throws IOException { + @RequestParam("file") MultipartFile file) { String imageUrl = productImageService.uploadImage(productId, file); return ResponseEntity.ok(imageUrl); diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java index e79f528..bfbcf11 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -4,6 +4,7 @@ import com.example.eightyage.domain.product.entity.ProductImage; import com.example.eightyage.domain.product.repository.ProductImageRepository; import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.global.exception.ProductImageUploadException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -33,28 +34,32 @@ public class ProductImageService { // 제품 이미지 업로드 @Transactional - public String uploadImage(Long productId, MultipartFile file) throws IOException { - String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); // 파일명 중복 방지 + public String uploadImage(Long productId, MultipartFile file) { + try{ + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); // 파일명 중복 방지 - // S3에 업로드 - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucket) - .key(fileName) - .contentType(file.getContentType()) - .build(), - RequestBody.fromInputStream(file.getInputStream(), file.getSize()) - ); + // S3에 업로드 + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); - // S3 이미지 URL 생성 - String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, fileName); + // S3 이미지 URL 생성 + String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, fileName); - // DB 저장 - Product product = productRepository.findProductByIdOrElseThrow(productId); - ProductImage productImage = new ProductImage(product, imageUrl); - productImageRepository.save(productImage); + // DB 저장 + Product product = productRepository.findProductByIdOrElseThrow(productId); + ProductImage productImage = new ProductImage(product, imageUrl); + productImageRepository.save(productImage); - return imageUrl; + return imageUrl; + } catch (IOException e) { + throw new ProductImageUploadException("이미지 업로드를 실패하였습니다: " + e.getMessage(), e); + } } // 제품 이미지 삭제 diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 650e95c..e16655d 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.example.eightyage.global.exception; +import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.global.entity.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -41,4 +42,9 @@ public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getMessage()); } + + @ExceptionHandler(ProductImageUploadException.class) + public ResponseEntity handleProductImageUploadException(ProductImageUploadException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } From fa3ab39f4cc1c8a263dab77974c04962354359fc Mon Sep 17 00:00:00 2001 From: peridot Date: Thu, 27 Mar 2025 11:15:23 +0900 Subject: [PATCH 077/164] =?UTF-8?q?feat(coupon):=20=EC=BF=A0=ED=8F=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(coupon): 쿠폰 기능 구현 --- build.gradle | 3 + .../coupon/controller/CouponController.java | 20 +++-- .../domain/coupon/entity/Coupon.java | 19 +++++ .../domain/coupon/entity/CouponState.java | 2 +- .../coupon/repository/CouponRepository.java | 5 ++ .../eightyage/domain/coupon/service/.gitkeep | 0 .../domain/coupon/service/CouponService.java | 76 +++++++++++++++++++ .../event/controller/EventController.java | 2 +- .../domain/event/service/CouponService.java | 31 -------- .../domain/event/service/EventService.java | 15 ++++ .../eightyage/global/config/RedisConfig.java | 26 +++++++ src/main/resources/application.yml | 5 ++ 12 files changed, 165 insertions(+), 39 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/service/.gitkeep create mode 100644 src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/service/CouponService.java create mode 100644 src/main/java/com/example/eightyage/global/config/RedisConfig.java diff --git a/build.gradle b/build.gradle index 1524b0a..93e6a8c 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,9 @@ dependencies { // spring cloud AWS S3 // implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index f2a1cfb..354b155 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,15 +1,14 @@ package com.example.eightyage.domain.coupon.controller; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.event.service.CouponService; +import com.example.eightyage.domain.coupon.service.CouponService; import com.example.eightyage.global.dto.AuthUser; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @RequestMapping("api/v1/coupon") @@ -19,8 +18,17 @@ public class CouponController { private final CouponService couponService; @PostMapping("/{eventId}") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable String eventId) { + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); } + @GetMapping("/my") + public ResponseEntity> getMyCoupons(@AuthenticationPrincipal AuthUser authUser) { + return ResponseEntity.ok(couponService.getMyCoupons(authUser)); + } + + @GetMapping("/{couponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { + return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); + } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java index 9a2affb..3b20f79 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.coupon.entity; +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; import com.example.eightyage.domain.event.entity.Event; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.entity.TimeStamped; @@ -27,4 +28,22 @@ public class Coupon extends TimeStamped { @ManyToOne private Event event; + + public Coupon(String couponCode, CouponState state, User user, Event event) { + this.couponCode = couponCode; + this.state = state; + this.user = user; + this.event = event; + } + + public CouponResponseDto toDto() { + return new CouponResponseDto( + this.couponCode, + this.state, + this.user.getNickname(), + this.event.getName(), + this.event.getStartDate(), + this.event.getEndDate() + ); + } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java index 057bf3f..221a935 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java @@ -2,5 +2,5 @@ public enum CouponState { VALID, - INVALIDATED + INVALID } diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java index d617d00..7226774 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java @@ -1,9 +1,14 @@ package com.example.eightyage.domain.coupon.repository; import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.coupon.entity.CouponState; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface CouponRepository extends JpaRepository { + boolean existsByUserIdAndEventId(Long userId, Long eventId); + List findAllByUserIdAndState(Long userId, CouponState state); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java new file mode 100644 index 0000000..f71d43d --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -0,0 +1,76 @@ +package com.example.eightyage.domain.coupon.service; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.repository.CouponRepository; +import com.example.eightyage.domain.event.entity.Event; +import com.example.eightyage.domain.event.repository.EventRepository; +import com.example.eightyage.domain.event.service.EventService; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.ForbiddenException; +import com.example.eightyage.global.util.RandomCodeGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CouponService { + + private final CouponRepository couponRepository; + private final EventRepository eventRepository; + private final EventService eventService; + private final StringRedisTemplate stringRedisTemplate; + + public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + // 수량 우선 차감 + Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); + if (remain == null || remain < 0) { // atomic? `DESC`? + throw new IllegalStateException("쿠폰 수량 소진"); + } + + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new IllegalArgumentException("Event not found")); + if (!eventService.isValidEvent(event)) { + throw new IllegalStateException("이벤트 기간이 아닙니다."); + } + + if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + throw new IllegalStateException("이미 쿠폰 발급 받은 사용자입니다."); + } + + // 쿠폰 발급 및 저장 + String couponCode = RandomCodeGenerator.generateCouponCode(10); + Coupon coupon = new Coupon(couponCode, CouponState.VALID, User.fromAuthUser(authUser), event); + + couponRepository.save(coupon); + + return coupon.toDto(); + } + + public List getMyCoupons(AuthUser authUser) { + List coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID); + + return coupons.stream().map(coupon -> coupon.toDto()).collect(Collectors.toList()); + } + + public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new IllegalArgumentException("Coupon not found")); + + if(!coupon.getState().equals(CouponState.VALID)) { + throw new IllegalStateException("이미 사용된 쿠폰입니다."); + } + + if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { + throw new ForbiddenException("본인의 쿠폰이 아닙니다."); + } + + return coupon.toDto(); + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 39bc520..e007cba 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/event") +@RequestMapping("/api/v1/events") @RequiredArgsConstructor public class EventController { diff --git a/src/main/java/com/example/eightyage/domain/event/service/CouponService.java b/src/main/java/com/example/eightyage/domain/event/service/CouponService.java deleted file mode 100644 index be0f3b8..0000000 --- a/src/main/java/com/example/eightyage/domain/event/service/CouponService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.global.dto.AuthUser; -import com.example.eightyage.global.util.RandomCodeGenerator; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CouponService { - - private final CouponRepository couponRepository; - private final RandomCodeGenerator randomCodeGenerator; - - public CouponResponseDto issueCoupon(AuthUser authUser, String eventId) { - // 해당 이벤트 조회 - - // 이벤트 상태 및 수량 확인 - - // 사용자 발급 여부 확인 - - // 랜덤 쿠폰 번호 생성 - String couponCode = randomCodeGenerator.generateCouponCode(10); - // 쿠폰 발급 (DB 저장) - - // 응답: 발급된 쿠폰 정보 - return new CouponResponseDto(); - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index 72d637a..edae337 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -9,6 +9,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -18,7 +20,9 @@ public class EventService { private final EventRepository eventRepository; + private final StringRedisTemplate stringRedisTemplate; + @Secured("ROLE_ADMIN") public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { Event event = new Event( eventRequestDto.getName(), @@ -31,6 +35,9 @@ public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { checkEventState(event); Event savedEvent = eventRepository.save(event); + + stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); + return savedEvent.toDto(); } @@ -53,6 +60,7 @@ public EventResponseDto getEvent(long eventId) { return event.toDto(); } + @Secured("ROLE_ADMIN") public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new IllegalArgumentException("Event not found")); @@ -77,4 +85,11 @@ private void checkEventState(Event event) { eventRepository.save(event); } } + + public boolean isValidEvent(Event event) { + LocalDateTime now = LocalDateTime.now(); + return ((event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && + (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) + ? true : false; + } } diff --git a/src/main/java/com/example/eightyage/global/config/RedisConfig.java b/src/main/java/com/example/eightyage/global/config/RedisConfig.java new file mode 100644 index 0000000..8373a4a --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/RedisConfig.java @@ -0,0 +1,26 @@ +package com.example.eightyage.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + return new StringRedisTemplate(redisConnectionFactory); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e592d69..1412129 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,6 +29,11 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect + data: + redis: + host: localhost + port: 6379 + jwt: secret: key: ${JWT_SECRET_KEY} From 33790daad07d669ae8eb8d9ee6523d861521de46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 11:46:15 +0900 Subject: [PATCH 078/164] =?UTF-8?q?fix(product):=20responseDto=20=EC=97=90?= =?UTF-8?q?=20=EB=B9=8C=EB=8D=94=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ProductGetResponseDto.java | 2 ++ .../dto/response/ProductSaveResponseDto.java | 4 +++ .../response/ProductUpdateResponseDto.java | 2 ++ .../product/service/ProductService.java | 30 +++++++++++++++++-- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java index 62b8b26..4b76fd7 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java @@ -3,11 +3,13 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ProductGetResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java index f526d92..552387e 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java @@ -3,11 +3,13 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ProductSaveResponseDto { @@ -17,6 +19,8 @@ public class ProductSaveResponseDto { private final Integer price; + private final String content; + private final SaleState saleState; private final LocalDateTime createdAt; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java index 50723a6..a2bf8f4 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java @@ -3,11 +3,13 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ProductUpdateResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 35caecd..61cf8b4 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -34,7 +34,15 @@ public ProductSaveResponseDto saveProduct(String productName, Category category, Product savedProduct = productRepository.save(product); - return new ProductSaveResponseDto(savedProduct.getName(), savedProduct.getCategory(), savedProduct.getPrice(), savedProduct.getSaleState(), savedProduct.getCreatedAt(), savedProduct.getModifiedAt()); + return ProductSaveResponseDto.builder() + .productName(savedProduct.getName()) + .category(savedProduct.getCategory()) + .price(savedProduct.getPrice()) + .content(savedProduct.getContent()) + .saleState(savedProduct.getSaleState()) + .createdAt(savedProduct.getCreatedAt()) + .modifiedAt(savedProduct.getModifiedAt()) + .build(); } // 제품 수정 @@ -48,7 +56,15 @@ public ProductUpdateResponseDto updateProduct(Long productId, String productName if(saleState != null) findProduct.setSaleState(saleState); if(price != null) findProduct.setPrice(price); - return new ProductUpdateResponseDto(findProduct.getName(), findProduct.getPrice(), findProduct.getContent(), findProduct.getCategory(), findProduct.getSaleState(), findProduct.getCreatedAt(), findProduct.getModifiedAt()); + return ProductUpdateResponseDto.builder() + .productName(findProduct.getName()) + .category(findProduct.getCategory()) + .price(findProduct.getPrice()) + .content(findProduct.getContent()) + .saleState(findProduct.getSaleState()) + .createdAt(findProduct.getCreatedAt()) + .modifiedAt(findProduct.getModifiedAt()) + .build(); } // 제품 단건 조회 @@ -56,7 +72,15 @@ public ProductUpdateResponseDto updateProduct(Long productId, String productName public ProductGetResponseDto findProductById(Long productId) { Product findProduct = productRepository.findProductByIdOrElseThrow(productId); - return new ProductGetResponseDto(findProduct.getName(), findProduct.getContent(), findProduct.getCategory(), findProduct.getPrice(), findProduct.getSaleState(), findProduct.getCreatedAt(), findProduct.getModifiedAt()); + return ProductGetResponseDto.builder() + .productName(findProduct.getName()) + .content(findProduct.getContent()) + .category(findProduct.getCategory()) + .price(findProduct.getPrice()) + .saleState(findProduct.getSaleState()) + .createdAt(findProduct.getCreatedAt()) + .modifiedAt(findProduct.getModifiedAt()) + .build(); } // 제품 삭제 From 80bd384ddf51abc50f1737daddba8f289d93a717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 11:46:28 +0900 Subject: [PATCH 079/164] =?UTF-8?q?fix(review):=20responseDto=20=EC=97=90?= =?UTF-8?q?=20=EB=B9=8C=EB=8D=94=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ReviewSaveResponseDto.java | 2 + .../dto/response/ReviewUpdateResponseDto.java | 2 + .../dto/response/ReviewsGetResponseDto.java | 2 + .../domain/review/service/ReviewService.java | 40 ++++++++++++++----- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java index 0602f73..03febd7 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java @@ -1,11 +1,13 @@ package com.example.eightyage.domain.review.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ReviewSaveResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java index ea21f04..9c387e5 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java @@ -1,11 +1,13 @@ package com.example.eightyage.domain.review.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ReviewUpdateResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java index 0af7647..c8742ce 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java @@ -1,11 +1,13 @@ package com.example.eightyage.domain.review.dto.response; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; @Getter +@Builder @AllArgsConstructor public class ReviewsGetResponseDto { diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 9ba5475..a04ce08 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.review.service; +import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; @@ -35,7 +36,16 @@ public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double scor Review review = new Review(findUser, findProduct, score, content); Review savedReview = reviewRepository.save(review); - return new ReviewSaveResponseDto(savedReview.getId(), findUser.getId(), findProduct.getId(), findUser.getNickname(), savedReview.getScore(), savedReview.getContent(), savedReview.getCreatedAt(), savedReview.getModifiedAt()); + return ReviewSaveResponseDto.builder() + .id(savedReview.getId()) + .userId(savedReview.getUser().getId()) + .productId(savedReview.getProduct().getId()) + .nickname(savedReview.getUser().getNickname()) + .score(savedReview.getScore()) + .content(savedReview.getContent()) + .createdAt(savedReview.getCreatedAt()) + .modifiedAt(savedReview.getModifiedAt()) + .build(); } // 리뷰 수정 @@ -49,7 +59,15 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double s if(score != null) findReview.setScore(score); } - return new ReviewUpdateResponseDto(findReview.getId(), userId, findUser.getNickname(), findReview.getScore(), findReview.getContent(), findReview.getCreatedAt(), findReview.getModifiedAt()); + return ReviewUpdateResponseDto.builder() + .id(findReview.getId()) + .userId(findUser.getId()) + .nickname(findUser.getNickname()) + .score(findReview.getScore()) + .content(findReview.getContent()) + .createdAt(findReview.getCreatedAt()) + .modifiedAt(findReview.getModifiedAt()) + .build(); } // 리뷰 다건 조회 @@ -57,15 +75,15 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double s public Page findReviews(Long productId, Pageable pageable) { Page reviewPage = reviewRepository.findByProductIdAndProductDeletedAtIsNull(productId, pageable); - return reviewPage.map(review -> new ReviewsGetResponseDto( - review.getId(), - review.getUser().getId(), - review.getUser().getNickname(), - review.getScore(), - review.getContent(), - review.getCreatedAt(), - review.getModifiedAt() - )); + return reviewPage.map(review -> ReviewsGetResponseDto.builder() + .id(review.getId()) + .userId(review.getUser().getId()) + .nickname(review.getUser().getNickname()) + .score(review.getScore()) + .content(review.getContent()) + .createdAt(review.getCreatedAt()) + .modifiedAt(review.getModifiedAt()) + .build()); } // 리뷰 삭제 From e21781a42d184de0fb9ca2c0335e0c6994074908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:06:16 +0900 Subject: [PATCH 080/164] =?UTF-8?q?refactor(product):=20setter=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20update=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/entity/Product.java | 39 ++++++++++++++++--- .../product/service/ProductService.java | 10 ++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 549fa4c..f4c64a1 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -21,18 +21,17 @@ public class Product extends TimeStamped { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Setter private String name; @Enumerated(EnumType.STRING) - @Setter private Category category; + private Category category; - @Setter private String content; + private String content; - @Setter private Integer price; + private Integer price; @Enumerated(EnumType.STRING) - @Setter private SaleState saleState; + private SaleState saleState; public Product(String name, Category category, String content, Integer price, SaleState saleState) { this.name = name; @@ -41,4 +40,34 @@ public Product(String name, Category category, String content, Integer price, Sa this.price = price; this.saleState = saleState; } + + public void updateName(String newName){ + if(newName != null){ + this.name = newName; + } + } + + public void updateCategory(Category newCategory) { + if (newCategory != null) { + this.category = newCategory; + } + } + + public void updateContent(String newContent) { + if (newContent != null) { + this.content = newContent; + } + } + + public void updatePrice(Integer newPrice) { + if (newPrice != null) { + this.price = newPrice; + } + } + + public void updateSaleState(SaleState newSaleState) { + if (newSaleState != null) { + this.saleState = newSaleState; + } + } } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 61cf8b4..5e515b5 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -50,11 +50,11 @@ public ProductSaveResponseDto saveProduct(String productName, Category category, public ProductUpdateResponseDto updateProduct(Long productId, String productName, Category category, String content, SaleState saleState, Integer price) { Product findProduct = productRepository.findProductByIdOrElseThrow(productId); - if(productName != null) findProduct.setName(productName); - if(category != null) findProduct.setCategory(category); - if(content != null) findProduct.setContent(content); - if(saleState != null) findProduct.setSaleState(saleState); - if(price != null) findProduct.setPrice(price); + findProduct.updateName(productName); + findProduct.updateCategory(category); + findProduct.updateContent(content); + findProduct.updateSaleState(saleState); + findProduct.updatePrice(price); return ProductUpdateResponseDto.builder() .productName(findProduct.getName()) From ff7ef420602f75917503b47b760e3a86ef6c1cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:06:42 +0900 Subject: [PATCH 081/164] =?UTF-8?q?refactor(review):=20setter=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20update=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/entity/Review.java | 16 ++++++++++++++-- .../domain/review/service/ReviewService.java | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/entity/Review.java b/src/main/java/com/example/eightyage/domain/review/entity/Review.java index 8bb4f09..f198fd1 100644 --- a/src/main/java/com/example/eightyage/domain/review/entity/Review.java +++ b/src/main/java/com/example/eightyage/domain/review/entity/Review.java @@ -26,9 +26,9 @@ public class Review extends TimeStamped { @JoinColumn(name = "product_id") private Product product; - @Setter private Double score; + private Double score; - @Setter private String content; + private String content; public Review(User user, Product product, Double score, String content) { this.user = user; @@ -36,4 +36,16 @@ public Review(User user, Product product, Double score, String content) { this.score = score; this.content = content; } + + public void updateScore(Double newScore){ + if(newScore != null){ + this.score = newScore; + } + } + + public void updateContent(String newContent){ + if(newContent != null){ + this.content = newContent; + } + } } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index a04ce08..c852a97 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -55,8 +55,8 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double s Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); if(findUser.getId() == findReview.getUser().getId()){ - if(content != null) findReview.setContent(content); - if(score != null) findReview.setScore(score); + findReview.updateScore(score); + findReview.updateContent(content); } return ReviewUpdateResponseDto.builder() From cfbcde985c3ecd0dc4094f27f2f7684677dad68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:24:53 +0900 Subject: [PATCH 082/164] =?UTF-8?q?refactor(review):=20pageable=20?= =?UTF-8?q?=EC=9D=84=20pageRequest=20=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/controller/ReviewController.java | 11 ++++------- .../domain/review/repository/ReviewRepository.java | 2 +- .../domain/review/service/ReviewService.java | 5 +++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index d1c4539..ee0040c 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -58,15 +58,12 @@ public ResponseEntity updateReview( public ResponseEntity> getReviews( @PathVariable Long productId, @RequestParam(required = false, defaultValue = "score") String orderBy, - @PageableDefault(size = 10) Pageable pageable + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size ){ - Pageable sortedPageable = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - Sort.by(Sort.Direction.DESC, orderBy) - ); + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, orderBy)); - Page reviews = reviewService.findReviews(productId, sortedPageable); + Page reviews = reviewService.findReviews(productId, pageRequest); return ResponseEntity.ok(reviews); } diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java index d02dccf..10528d2 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -25,6 +25,6 @@ default Review findReviewByIdOrElseThrow(Long reviewId){ Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); - @Query("SELECT r FROM Review r WHERE r.product.id = :productId") + @Query("SELECT r FROM Review r WHERE r.product.id = :productId AND r.deletedAt IS NULL") List findReviewsByProductId(@Param("productId") Long productId); } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index c852a97..4f1138a 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -12,6 +12,7 @@ import com.example.eightyage.domain.user.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -72,8 +73,8 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double s // 리뷰 다건 조회 @Transactional(readOnly = true) - public Page findReviews(Long productId, Pageable pageable) { - Page reviewPage = reviewRepository.findByProductIdAndProductDeletedAtIsNull(productId, pageable); + public Page findReviews(Long productId, PageRequest pageRequest) { + Page reviewPage = reviewRepository.findByProductIdAndProductDeletedAtIsNull(productId, pageRequest); return reviewPage.map(review -> ReviewsGetResponseDto.builder() .id(review.getId()) From 90868eb049e4f09836b2035858f38c62f3df5393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:30:56 +0900 Subject: [PATCH 083/164] =?UTF-8?q?feat(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=8B=B9=EC=82=AC=EC=9E=90=EA=B0=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=8D=98?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/service/ReviewService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 4f1138a..1dca55b 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -10,6 +10,7 @@ import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -55,9 +56,11 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double s User findUser = userService.findUserByIdOrElseThrow(userId); Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); - if(findUser.getId() == findReview.getUser().getId()){ + if(findUser.getId().equals(findReview.getUser().getId())){ findReview.updateScore(score); findReview.updateContent(content); + } else { + throw new UnauthorizedException("리뷰를 수정할 권한이 없습니다."); } return ReviewUpdateResponseDto.builder() From b8e9aaf594e134e49c4029ad1986ede05843e2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:41:01 +0900 Subject: [PATCH 084/164] =?UTF-8?q?feat(productImage):=20ProductImageUploa?= =?UTF-8?q?dException=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=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 --- .../global/exception/ProductImageUploadException.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/com/example/eightyage/global/exception/ProductImageUploadException.java diff --git a/src/main/java/com/example/eightyage/global/exception/ProductImageUploadException.java b/src/main/java/com/example/eightyage/global/exception/ProductImageUploadException.java new file mode 100644 index 0000000..a12b4c0 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/exception/ProductImageUploadException.java @@ -0,0 +1,7 @@ +package com.example.eightyage.global.exception; + +public class ProductImageUploadException extends RuntimeException { + public ProductImageUploadException(String message, Throwable cause){ + super(message, cause); + } +} From 1eff5a2042f6367d1704616883545c7c69c453d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:41:46 +0900 Subject: [PATCH 085/164] =?UTF-8?q?refactor(productImage):=20setter=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20deletedAt=20=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/service/ProductImageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java index bfbcf11..34656f8 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -67,7 +67,7 @@ public String uploadImage(Long productId, MultipartFile file) { public void deleteImage(Long imageId) { ProductImage findProductImage = productImageRepository.findProductImageByIdOrElseThrow(imageId); - findProductImage.setDeletedAt(LocalDateTime.now()); + findProductImage.delete(); } } From e38d56be60956ee66cbc9671210712ebae871900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:41:57 +0900 Subject: [PATCH 086/164] =?UTF-8?q?refactor(product):=20setter=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20deletedAt=20=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/service/ProductService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 5e515b5..396f381 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -90,9 +90,9 @@ public void deleteProduct(Long productId) { List findReviewList = reviewRepository.findReviewsByProductId(productId); for(Review review : findReviewList){ - review.setDeletedAt(LocalDateTime.now()); + review.delete(); } - findProduct.setDeletedAt(LocalDateTime.now()); + findProduct.delete(); } } From 96f4e96a83e1163d3da4bc726bbffbaac55e32be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 12:42:08 +0900 Subject: [PATCH 087/164] =?UTF-8?q?refactor(review):=20setter=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20deletedAt=20=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/service/ReviewService.java | 6 ++++-- .../com/example/eightyage/global/entity/TimeStamped.java | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 1dca55b..2528561 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -96,8 +96,10 @@ public void deleteReview(Long userId, Long reviewId) { User findUser = userService.findUserByIdOrElseThrow(userId); Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); - if(findUser.getId() == findReview.getUser().getId()){ - findReview.setDeletedAt(LocalDateTime.now()); + if(findUser.getId().equals(findReview.getUser().getId())){ + findReview.delete(); + } else { + throw new UnauthorizedException("리뷰를 삭제할 권한이 없습니다."); } } } diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index 1a49cc9..27b911b 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -28,6 +28,10 @@ public abstract class TimeStamped { @Temporal(TemporalType.TIMESTAMP) private LocalDateTime deletedAt; + public void delete() { + this.deletedAt = LocalDateTime.now(); + } + public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; } From c73dc860d50796ea1f9ee25f0881ba04206d7090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 13:10:43 +0900 Subject: [PATCH 088/164] =?UTF-8?q?refactor(productImage):=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=EB=A1=9C=EC=A7=81=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=8B=A4=EB=A3=A8=EB=8A=94=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/repository/ProductImageRepository.java | 4 ---- .../domain/product/service/ProductImageService.java | 13 ++++++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java index 1d47f09..a29cc6c 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java @@ -12,8 +12,4 @@ public interface ProductImageRepository extends JpaRepository findById(Long imageId); - - default ProductImage findProductImageByIdOrElseThrow(Long imageId){ - return findById(imageId).orElseThrow(() -> new NotFoundException("해당 이미지가 존재하지 않습니다.")); - } } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java index 34656f8..2fb20bc 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -4,6 +4,7 @@ import com.example.eightyage.domain.product.entity.ProductImage; import com.example.eightyage.domain.product.repository.ProductImageRepository; import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.ProductImageUploadException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -24,7 +25,7 @@ public class ProductImageService { private final S3Client s3Client; private final ProductImageRepository productImageRepository; - private final ProductRepository productRepository; + private final ProductService productService; @Value("${aws.s3.bucket}") private String bucket; @@ -52,7 +53,7 @@ public String uploadImage(Long productId, MultipartFile file) { String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, fileName); // DB 저장 - Product product = productRepository.findProductByIdOrElseThrow(productId); + Product product = productService.findProductByIdOrElseThrow(productId); ProductImage productImage = new ProductImage(product, imageUrl); productImageRepository.save(productImage); @@ -65,9 +66,15 @@ public String uploadImage(Long productId, MultipartFile file) { // 제품 이미지 삭제 @Transactional public void deleteImage(Long imageId) { - ProductImage findProductImage = productImageRepository.findProductImageByIdOrElseThrow(imageId); + ProductImage findProductImage = findProductImageByIdOrElseThrow(imageId); findProductImage.delete(); } + + public ProductImage findProductImageByIdOrElseThrow(Long imageId){ + return productImageRepository.findById(imageId).orElseThrow( + () -> new NotFoundException("해당 이미지가 존재하지 않습니다.") + ); + } } From 82b417110161123aa7df640d18dbc631b9982705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 13:11:00 +0900 Subject: [PATCH 089/164] =?UTF-8?q?refactor(product):=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=EB=A1=9C=EC=A7=81=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=A3=A8=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/repository/ProductRepository.java | 4 ---- .../domain/product/service/ProductService.java | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index 08ac537..e2524ff 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -15,8 +15,4 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE p.id = :productId AND p.deletedAt IS NULL") Optional findById(@Param("productId") Long productId); - - default Product findProductByIdOrElseThrow(Long productId){ - return findById(productId).orElseThrow(() -> new NotFoundException("해당 제품이 존재하지 않습니다.")); - } } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 396f381..52be317 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -9,6 +9,7 @@ import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; +import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; @@ -20,6 +21,8 @@ import java.util.Optional; import java.util.Set; +import static com.example.eightyage.global.exception.ErrorMessage.USER_EMAIL_NOT_FOUND; + @Service @RequiredArgsConstructor public class ProductService { @@ -48,7 +51,7 @@ public ProductSaveResponseDto saveProduct(String productName, Category category, // 제품 수정 @Transactional public ProductUpdateResponseDto updateProduct(Long productId, String productName, Category category, String content, SaleState saleState, Integer price) { - Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + Product findProduct = findProductByIdOrElseThrow(productId); findProduct.updateName(productName); findProduct.updateCategory(category); @@ -70,7 +73,7 @@ public ProductUpdateResponseDto updateProduct(Long productId, String productName // 제품 단건 조회 @Transactional(readOnly = true) public ProductGetResponseDto findProductById(Long productId) { - Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + Product findProduct = findProductByIdOrElseThrow(productId); return ProductGetResponseDto.builder() .productName(findProduct.getName()) @@ -86,7 +89,7 @@ public ProductGetResponseDto findProductById(Long productId) { // 제품 삭제 @Transactional public void deleteProduct(Long productId) { - Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + Product findProduct = findProductByIdOrElseThrow(productId); List findReviewList = reviewRepository.findReviewsByProductId(productId); for(Review review : findReviewList){ @@ -95,4 +98,10 @@ public void deleteProduct(Long productId) { findProduct.delete(); } + + public Product findProductByIdOrElseThrow(Long productId) { + return productRepository.findById(productId).orElseThrow( + () -> new NotFoundException("해당 제품이 존재하지 않습니다.") + ); + } } From 9ec24d8c0f0be9c2a4db34d769ee50224ce7531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 13:11:12 +0900 Subject: [PATCH 090/164] =?UTF-8?q?refactor(review):=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=EB=A1=9C=EC=A7=81=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=A3=A8=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/repository/ReviewRepository.java | 4 ---- .../domain/review/service/ReviewService.java | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java index 10528d2..062b6e5 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -19,10 +19,6 @@ public interface ReviewRepository extends JpaRepository { @Query("SELECT r FROM Review r WHERE r.id = :reviewId AND r.deletedAt IS NULL") Optional findById(@Param("reviewId") Long reviewId); - default Review findReviewByIdOrElseThrow(Long reviewId){ - return findById(reviewId).orElseThrow(() -> new NotFoundException("해당 리뷰가 존재하지 않습니다.")); - } - Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); @Query("SELECT r FROM Review r WHERE r.product.id = :productId AND r.deletedAt IS NULL") diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 2528561..2fcf099 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.domain.product.service.ProductService; import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; import com.example.eightyage.domain.review.dto.response.ReviewUpdateResponseDto; import com.example.eightyage.domain.review.dto.response.ReviewsGetResponseDto; @@ -10,6 +11,7 @@ import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -27,13 +29,13 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final UserService userService; - private final ProductRepository productRepository; + private final ProductService productService; // 리뷰 생성 @Transactional public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double score, String content) { User findUser = userService.findUserByIdOrElseThrow(userId); - Product findProduct = productRepository.findProductByIdOrElseThrow(productId); + Product findProduct = productService.findProductByIdOrElseThrow(productId); Review review = new Review(findUser, findProduct, score, content); Review savedReview = reviewRepository.save(review); @@ -54,7 +56,7 @@ public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double scor @Transactional public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double score, String content) { User findUser = userService.findUserByIdOrElseThrow(userId); - Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); + Review findReview = findReviewByIdOrElseThrow(reviewId); if(findUser.getId().equals(findReview.getUser().getId())){ findReview.updateScore(score); @@ -94,7 +96,7 @@ public Page findReviews(Long productId, PageRequest pageR @Transactional public void deleteReview(Long userId, Long reviewId) { User findUser = userService.findUserByIdOrElseThrow(userId); - Review findReview = reviewRepository.findReviewByIdOrElseThrow(reviewId); + Review findReview = findReviewByIdOrElseThrow(reviewId); if(findUser.getId().equals(findReview.getUser().getId())){ findReview.delete(); @@ -102,4 +104,10 @@ public void deleteReview(Long userId, Long reviewId) { throw new UnauthorizedException("리뷰를 삭제할 권한이 없습니다."); } } + + public Review findReviewByIdOrElseThrow(Long reviewId){ + return reviewRepository.findById(reviewId).orElseThrow( + () -> new NotFoundException("해당 리뷰가 존재하지 않습니다.") + ); + } } From f96533c2bd869781b003f7a9afce91ee64a31c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 13:22:34 +0900 Subject: [PATCH 091/164] =?UTF-8?q?fix(review):=20JOIN=20FETCH=20=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20N+1=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/repository/ReviewRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java index 062b6e5..24716be 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -5,6 +5,7 @@ import com.example.eightyage.global.exception.NotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -21,6 +22,6 @@ public interface ReviewRepository extends JpaRepository { Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); - @Query("SELECT r FROM Review r WHERE r.product.id = :productId AND r.deletedAt IS NULL") + @Query("SELECT r FROM Review r JOIN FETCH r.user JOIN FETCH r.product WHERE r.product.id = :productId AND r.deletedAt IS NULL") List findReviewsByProductId(@Param("productId") Long productId); } From bb9ecdcb410bf150677c19051f29ba111cc38851 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Thu, 27 Mar 2025 14:05:40 +0900 Subject: [PATCH 092/164] =?UTF-8?q?fix(auth):=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=EC=8B=9C=20403=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 개선한 것 - 로그인을 했다면 refresh 토큰 재발급 가능 - 권한이 없는 유저가 접근했을 때 500에러가 발생한 것을 403에러가 출력되도록 GlobalExceptionHandler 수정 --- .../domain/auth/controller/AuthController.java | 5 +++++ .../global/exception/GlobalExceptionHandler.java | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index aec6a97..9ae8d48 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -10,8 +10,12 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; +import static com.example.eightyage.domain.user.entity.UserRole.Authority.ADMIN; +import static com.example.eightyage.domain.user.entity.UserRole.Authority.USER; + @RestController @RequiredArgsConstructor @RequestMapping("/api") @@ -47,6 +51,7 @@ public AuthAccessTokenResponseDto signin( } /* 토큰 재발급 (로그인 기간 연장) */ + @Secured({USER, ADMIN}) @GetMapping("/v1/auth/refresh") public AuthAccessTokenResponseDto refresh( @RefreshToken String refreshToken, diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 650e95c..6376e06 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -12,6 +13,7 @@ import java.util.List; +import static com.example.eightyage.global.exception.ErrorMessage.DEFAULT_FORBIDDEN; import static com.example.eightyage.global.exception.ErrorMessage.INTERNAL_SERVER_ERROR; @Slf4j @@ -35,10 +37,18 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa return ErrorResponse.of(HttpStatus.BAD_REQUEST, validFailedList); } + @ResponseStatus(value = HttpStatus.FORBIDDEN) + @ExceptionHandler(AccessDeniedException.class) + public ErrorResponse handleAccessDeniedException() { + return ErrorResponse.of(HttpStatus.FORBIDDEN, DEFAULT_FORBIDDEN.getMessage()); + } + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { log.error("Exception : {}",e.getMessage(), e); return ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR.getMessage()); } + + } From 8133856a5dd15b1afac14f5bd1f82c11eca6df42 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Thu, 27 Mar 2025 14:10:16 +0900 Subject: [PATCH 093/164] =?UTF-8?q?fix(auth):=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=EC=8B=9C=20403=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 개선한 것 - 로그인을 했다면 refresh 토큰 재발급 가능 - 권한이 없는 유저가 접근했을 때 500에러가 발생한 것을 403에러가 출력되도록 GlobalExceptionHandler 수정 --- .../eightyage/domain/auth/controller/AuthController.java | 5 +++++ .../global/exception/GlobalExceptionHandler.java | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index aec6a97..9ae8d48 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -10,8 +10,12 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; +import static com.example.eightyage.domain.user.entity.UserRole.Authority.ADMIN; +import static com.example.eightyage.domain.user.entity.UserRole.Authority.USER; + @RestController @RequiredArgsConstructor @RequestMapping("/api") @@ -47,6 +51,7 @@ public AuthAccessTokenResponseDto signin( } /* 토큰 재발급 (로그인 기간 연장) */ + @Secured({USER, ADMIN}) @GetMapping("/v1/auth/refresh") public AuthAccessTokenResponseDto refresh( @RefreshToken String refreshToken, diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index e16655d..62af117 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -1,10 +1,10 @@ package com.example.eightyage.global.exception; -import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.global.entity.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,6 +13,7 @@ import java.util.List; +import static com.example.eightyage.global.exception.ErrorMessage.DEFAULT_FORBIDDEN; import static com.example.eightyage.global.exception.ErrorMessage.INTERNAL_SERVER_ERROR; @Slf4j @@ -36,6 +37,12 @@ public ErrorResponse> handleValidationException(MethodArgumentNotVa return ErrorResponse.of(HttpStatus.BAD_REQUEST, validFailedList); } + @ResponseStatus(value = HttpStatus.FORBIDDEN) + @ExceptionHandler(AccessDeniedException.class) + public ErrorResponse handleAccessDeniedException() { + return ErrorResponse.of(HttpStatus.FORBIDDEN, DEFAULT_FORBIDDEN.getMessage()); + } + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ErrorResponse handleGlobalException(Exception e) { From 67d0fa76e6d017015f3453b5c4944b71bb0d82fb Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Thu, 27 Mar 2025 15:15:15 +0900 Subject: [PATCH 094/164] =?UTF-8?q?chore(search):=20product=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EA=B3=BC=20=EB=B3=91=ED=95=A9=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 25 +++++++++++ .../response/ProductSearchResponseDto.java | 27 ++++++++++++ .../product/repository/ProductRepository.java | 15 ++++++- .../product/service/ProductService.java | 44 ++++++++++++++++--- .../dto/ProductSearchResponse.java | 27 ------------ .../search/fakeProduct/entity/Category.java | 6 --- .../fakeProduct/entity/FakeProduct.java | 40 ----------------- .../search/fakeProduct/entity/SaleState.java | 6 --- .../repository/ProductRepository.java | 23 ---------- .../v1/controller/ProductControllerV1.java | 28 ------------ .../v1/service/ProductServiceV1.java | 38 ---------------- .../v2/controller/ProductControllerV2.java | 28 ------------ .../v2/service/ProductServiceV2.java | 36 --------------- 13 files changed, 103 insertions(+), 240 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index bcf8666..31a1da2 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -4,10 +4,13 @@ import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; +import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.service.ProductService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; @@ -49,6 +52,28 @@ public ResponseEntity getProduct(@PathVariable Long produ return ResponseEntity.ok(responseDto); } + // 제품 다건 조회 version 1 + @GetMapping("/v1/products") + public ResponseEntity> searchProductV1( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProductsV1(name, category, size, page)); + } + + // 제품 다건 조회 version 2 + @GetMapping("/v2/products") + public ResponseEntity> searchProductV2( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProductsV2(name, category, size, page)); + } + // 제품 삭제 @Secured("ROLE_ADMIN") @DeleteMapping("/v1/products/{productId}") diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java new file mode 100644 index 0000000..2e3c0a9 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java @@ -0,0 +1,27 @@ +package com.example.eightyage.domain.product.dto.response; + +import com.example.eightyage.domain.product.entity.Product; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Builder +@Getter +public class ProductSearchResponseDto { + private final String productName; + private final String category; + private final Integer price; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public static ProductSearchResponseDto from(Product product) { + return ProductSearchResponseDto.builder() + .productName(product.getName()) + .category(product.getCategory().toString()) + .price(product.getPrice()) + .createdAt(product.getCreatedAt()) + .updatedAt(product.getModifiedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index e2524ff..da60e83 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -1,8 +1,9 @@ package com.example.eightyage.domain.product.repository; +import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.review.entity.Review; -import com.example.eightyage.global.exception.NotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -15,4 +16,14 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE p.id = :productId AND p.deletedAt IS NULL") Optional findById(@Param("productId") Long productId); + + @Query("SELECT p FROM Product p WHERE p.saleState = 'FOR_SALE' " + + "AND (:category IS NULL OR p.category = :category) " + + "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + + "ORDER BY p.name") + Page findProducts( + @Param("name")String name, + @Param("category") Category category, + Pageable pageable + ); } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 52be317..ffb867c 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -2,6 +2,7 @@ import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.Product; @@ -9,19 +10,20 @@ import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; -import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.search.v1.service.SearchServiceV1; +import com.example.eightyage.domain.search.v2.service.SearchServiceV2; import com.example.eightyage.global.exception.NotFoundException; -import com.example.eightyage.global.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; -import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; -import java.util.Set; -import static com.example.eightyage.global.exception.ErrorMessage.USER_EMAIL_NOT_FOUND; @Service @RequiredArgsConstructor @@ -29,6 +31,8 @@ public class ProductService { private final ProductRepository productRepository; private final ReviewRepository reviewRepository; + private final SearchServiceV1 searchServiceV1; + private final SearchServiceV2 searchServiceV2; // 제품 생성 @Transactional @@ -86,6 +90,34 @@ public ProductGetResponseDto findProductById(Long productId) { .build(); } + // 제품 다건 조회 version 1 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Page getProductsV1(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page products = productRepository.findProducts(productName, category, pageable); + + if (StringUtils.hasText(productName) && !products.isEmpty()) { + searchServiceV1.saveSearchLog(productName); // 로그만 저장 + } + + return products.map(ProductSearchResponseDto::from); + } + + // 제품 다건 조회 version 2 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Page getProductsV2(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page products = productRepository.findProducts(productName, category, pageable); + + if (StringUtils.hasText(productName) && !products.isEmpty()) { + searchServiceV2.logAndCountKeyword(productName); // 로그 저장 + 캐시 작업 + } + + return products.map(ProductSearchResponseDto::from); + } + // 제품 삭제 @Transactional public void deleteProduct(Long productId) { diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java deleted file mode 100644 index 7f82943..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/dto/ProductSearchResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.dto; - -import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Builder -@Getter -public class ProductSearchResponse { - private final String productName; - private final String category; - private final Long price; - private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; - - public static ProductSearchResponse from(FakeProduct fakeProduct) { - return ProductSearchResponse.builder() - .productName(fakeProduct.getName()) - .category(fakeProduct.getCategory().toString()) - .price(fakeProduct.getPrice()) - .createdAt(fakeProduct.getCreatedAt()) - .updatedAt(fakeProduct.getUpdatedAt()) - .build(); - } -} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java deleted file mode 100644 index f1323b0..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/Category.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.entity; - -public enum Category { - SKIN, - LOTION -} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java deleted file mode 100644 index f896682..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/FakeProduct.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Entity -@NoArgsConstructor -@Getter -@EntityListeners(AuditingEntityListener.class) -public class FakeProduct { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - Long id; - - private String name; - - @Enumerated(EnumType.STRING) - private Category category; - - private Long price; - - @Column(name = "sale_state") - @Enumerated(EnumType.STRING) - private SaleState saleState; - - @CreatedDate - @Column(updatable = false) - private LocalDateTime createdAt; - - @LastModifiedDate - @Column - private LocalDateTime updatedAt; - -} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java deleted file mode 100644 index 121b169..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/entity/SaleState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.entity; - -public enum SaleState { - ON_SALE, - NOT_SALE -} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java deleted file mode 100644 index fe28445..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/repository/ProductRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.repository; - -import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface ProductRepository extends JpaRepository { - - @Query("SELECT p FROM FakeProduct p WHERE p.saleState = 'ON_SALE' " + - "AND (:category IS NULL OR p.category = :category) " + - "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + - "ORDER BY p.name") - Page findProducts( - @Param("name")String name, - @Param("category") Category category, - Pageable pageable - ); - -} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java deleted file mode 100644 index 0fdfd99..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/controller/ProductControllerV1.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.v1.controller; - -import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; -import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.v1.service.ProductServiceV1; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -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; - -@RestController -@RequiredArgsConstructor -public class ProductControllerV1 { - - private final ProductServiceV1 productService; - - @GetMapping("/api/v1/products") - public ResponseEntity> searchProduct( - @RequestParam(required = false) String name, - @RequestParam(required = false) Category category, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "1") int page - ) { - return ResponseEntity.ok(productService.getProducts(name, category, size, page)); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java deleted file mode 100644 index ad20ea0..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v1/service/ProductServiceV1.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.v1.service; - -import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; -import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; -import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; -import com.example.eightyage.domain.search.v1.service.SearchServiceV1; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -@Service -@RequiredArgsConstructor -public class ProductServiceV1 { - - private final ProductRepository productRepository; - private final SearchServiceV1 searchService; - - @Transactional(propagation = Propagation.REQUIRES_NEW) - public Page getProducts(String productName, Category category, int size, int page) { - int adjustedPage = Math.max(0, page - 1); - Pageable pageable = PageRequest.of(adjustedPage, size); - Page products = productRepository.findProducts(productName, category, pageable); - - if (StringUtils.hasText(productName) && !products.isEmpty()) { - searchService.saveSearchLog(productName); - } - - return products.map(ProductSearchResponse::from); - } - - -} diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java deleted file mode 100644 index 01acea1..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/controller/ProductControllerV2.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.v2.controller; - -import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; -import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.v2.service.ProductServiceV2; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -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; - -@RestController -@RequiredArgsConstructor -public class ProductControllerV2 { - - private final ProductServiceV2 productService; - - @GetMapping("/api/v2/products") - public ResponseEntity> searchProduct( - @RequestParam(required = false) String name, - @RequestParam(required = false) Category category, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "1") int page - ) { - return ResponseEntity.ok(productService.getProducts(name, category, size, page)); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java b/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java deleted file mode 100644 index 5627f88..0000000 --- a/src/main/java/com/example/eightyage/domain/search/fakeProduct/v2/service/ProductServiceV2.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.eightyage.domain.search.fakeProduct.v2.service; - -import com.example.eightyage.domain.search.fakeProduct.dto.ProductSearchResponse; -import com.example.eightyage.domain.search.fakeProduct.entity.Category; -import com.example.eightyage.domain.search.fakeProduct.entity.FakeProduct; -import com.example.eightyage.domain.search.fakeProduct.repository.ProductRepository; -import com.example.eightyage.domain.search.v2.service.SearchServiceV2; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -@Service -@RequiredArgsConstructor -public class ProductServiceV2 { - - private final ProductRepository productRepository; - private final SearchServiceV2 searchService; - - @Transactional(propagation = Propagation.REQUIRES_NEW) - public Page getProducts(String productName, Category category, int size, int page) { - int adjustedPage = Math.max(0, page - 1); - Pageable pageable = PageRequest.of(adjustedPage, size); - Page products = productRepository.findProducts(productName, category, pageable); - - if (StringUtils.hasText(productName) && !products.isEmpty()) { - searchService.logAndCountKeyword(productName); - } - - return products.map(ProductSearchResponse::from); - } -} From fefd9ece8b3e75cf857b62fbe628603375d644d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 15:18:11 +0900 Subject: [PATCH 095/164] =?UTF-8?q?build(gradle):=20dotenv=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ gradlew | 0 2 files changed, 3 insertions(+) mode change 100644 => 100755 gradlew diff --git a/build.gradle b/build.gradle index f55cac0..7ef2fa9 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,9 @@ dependencies { // spring cloud AWS S3 implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' + + // env + implementation 'io.github.cdimascio:java-dotenv:5.2.2' } tasks.named('test') { diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 83c67961385e936194d8b793ef6ea09118ecb7ee Mon Sep 17 00:00:00 2001 From: peridot Date: Thu, 27 Mar 2025 15:34:38 +0900 Subject: [PATCH 096/164] =?UTF-8?q?fix(coupon):=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 16 ++++--- .../domain/coupon/entity/Coupon.java | 21 +++++--- .../coupon/repository/CouponRepository.java | 4 +- .../domain/coupon/service/CouponService.java | 48 +++++++++---------- .../event/controller/EventController.java | 10 ++-- .../event/dto/request/EventRequestDto.java | 13 +++-- .../domain/event/service/EventService.java | 34 ++++++++----- .../global/dto/ValidationMessage.java | 5 ++ .../global/exception/ErrorMessage.java | 10 +++- 9 files changed, 100 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index 354b155..a57223a 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -4,6 +4,7 @@ import com.example.eightyage.domain.coupon.service.CouponService; import com.example.eightyage.global.dto.AuthUser; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -11,23 +12,26 @@ import java.util.List; @RestController -@RequestMapping("api/v1/coupon") +@RequestMapping("/api") @RequiredArgsConstructor public class CouponController { private final CouponService couponService; - @PostMapping("/{eventId}") + @PostMapping("/v1/events/{eventId}/coupons") public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); } - @GetMapping("/my") - public ResponseEntity> getMyCoupons(@AuthenticationPrincipal AuthUser authUser) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser)); + @GetMapping("/v1/coupons/my") + public ResponseEntity> getMyCoupons( + @AuthenticationPrincipal AuthUser authUser, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); } - @GetMapping("/{couponId}") + @GetMapping("/v1/coupons/{couponId}") public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java index 3b20f79..76b1b10 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -4,13 +4,18 @@ import com.example.eightyage.domain.event.entity.Event; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.entity.TimeStamped; +import com.example.eightyage.global.util.RandomCodeGenerator; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity +@Builder @Getter @NoArgsConstructor +@AllArgsConstructor public class Coupon extends TimeStamped { @Id @@ -23,17 +28,19 @@ public class Coupon extends TimeStamped { @Enumerated(EnumType.STRING) private CouponState state; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private User user; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private Event event; - public Coupon(String couponCode, CouponState state, User user, Event event) { - this.couponCode = couponCode; - this.state = state; - this.user = user; - this.event = event; + public static Coupon create(User user, Event event) { + return Coupon.builder() + .couponCode(RandomCodeGenerator.generateCouponCode(10)) + .state(CouponState.VALID) + .user(user) + .event(event) + .build(); } public CouponResponseDto toDto() { diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java index 7226774..a6c5dbd 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java @@ -2,6 +2,8 @@ import com.example.eightyage.domain.coupon.entity.Coupon; import com.example.eightyage.domain.coupon.entity.CouponState; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -10,5 +12,5 @@ @Repository public interface CouponRepository extends JpaRepository { boolean existsByUserIdAndEventId(Long userId, Long eventId); - List findAllByUserIdAndState(Long userId, CouponState state); + Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index f71d43d..81a2d66 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -5,25 +5,25 @@ import com.example.eightyage.domain.coupon.entity.CouponState; import com.example.eightyage.domain.coupon.repository.CouponRepository; import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.repository.EventRepository; import com.example.eightyage.domain.event.service.EventService; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.util.RandomCodeGenerator; +import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class CouponService { private final CouponRepository couponRepository; - private final EventRepository eventRepository; private final EventService eventService; private final StringRedisTemplate stringRedisTemplate; @@ -31,46 +31,46 @@ public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { // 수량 우선 차감 Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); if (remain == null || remain < 0) { // atomic? `DESC`? - throw new IllegalStateException("쿠폰 수량 소진"); + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); } - Event event = eventRepository.findById(eventId) - .orElseThrow(() -> new IllegalArgumentException("Event not found")); - if (!eventService.isValidEvent(event)) { - throw new IllegalStateException("이벤트 기간이 아닙니다."); - } + Event event = eventService.getValidEventOrThrow(eventId); if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new IllegalStateException("이미 쿠폰 발급 받은 사용자입니다."); + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } // 쿠폰 발급 및 저장 - String couponCode = RandomCodeGenerator.generateCouponCode(10); - Coupon coupon = new Coupon(couponCode, CouponState.VALID, User.fromAuthUser(authUser), event); + Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); couponRepository.save(coupon); return coupon.toDto(); } - public List getMyCoupons(AuthUser authUser) { - List coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID); + public Page getMyCoupons(AuthUser authUser, int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); - return coupons.stream().map(coupon -> coupon.toDto()).collect(Collectors.toList()); + return coupons.map(Coupon::toDto); } public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = couponRepository.findById(couponId) - .orElseThrow(() -> new IllegalArgumentException("Coupon not found")); + Coupon coupon = findByIdOrElseThrow(couponId); - if(!coupon.getState().equals(CouponState.VALID)) { - throw new IllegalStateException("이미 사용된 쿠폰입니다."); + if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { + throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); } - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException("본인의 쿠폰이 아닙니다."); + if(!coupon.getState().equals(CouponState.VALID)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); } return coupon.toDto(); } + + public Coupon findByIdOrElseThrow(Long couponId) { + return couponRepository.findById(couponId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + } } diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index e007cba..7c12b97 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -9,28 +9,28 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/events") +@RequestMapping("/api") @RequiredArgsConstructor public class EventController { private final EventService eventService; - @PostMapping + @PostMapping("/v1/events") public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); } - @GetMapping + @GetMapping("/v1/events") public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return ResponseEntity.ok(eventService.getEvents(page, size)); } - @GetMapping("/{eventId}") + @GetMapping("/v1/events/{eventId}") public ResponseEntity getEvent(@PathVariable long eventId) { return ResponseEntity.ok(eventService.getEvent(eventId)); } - @PatchMapping("/{eventId}") + @PatchMapping("/v1/events/{eventId}") public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); } diff --git a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java b/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java index c0a6ce4..381bd19 100644 --- a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java @@ -1,6 +1,9 @@ package com.example.eightyage.domain.event.dto.request; +import com.example.eightyage.global.dto.ValidationMessage; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,18 +15,18 @@ @AllArgsConstructor public class EventRequestDto { - @NotBlank + @NotBlank(message = ValidationMessage.NOT_BLANK_EVENT_NAME) private String name; - @NotBlank + @NotBlank(message = ValidationMessage.NOT_BLANK_EVENT_DESCRIPTION) private String description; - @NotBlank + @Min(value = 1, message = ValidationMessage.INVALID_EVENT_QUANTITY) private int quantity; - @NotBlank + @NotNull(message = ValidationMessage.NOT_NULL_START_DATE) private LocalDateTime startDate; - @NotBlank + @NotNull(message = ValidationMessage.NOT_NULL_END_DATE) private LocalDateTime endDate; } diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index edae337..a96f6e1 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -5,6 +5,8 @@ import com.example.eightyage.domain.event.entity.Event; import com.example.eightyage.domain.event.entity.EventState; import com.example.eightyage.domain.event.repository.EventRepository; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -22,7 +24,7 @@ public class EventService { private final EventRepository eventRepository; private final StringRedisTemplate stringRedisTemplate; - @Secured("ROLE_ADMIN") + @Secured("ADMIN") public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { Event event = new Event( eventRequestDto.getName(), @@ -42,7 +44,7 @@ public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { } public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page, size); + Pageable pageable = PageRequest.of(page-1, size); Page events = eventRepository.findAll(pageable); // 모든 events들 checkState로 state 상태 갱신하기 @@ -52,18 +54,16 @@ public Page getEvents(int page, int size) { } public EventResponseDto getEvent(long eventId) { - Event event = eventRepository.findById(eventId) - .orElseThrow(() -> new IllegalArgumentException("Event not found")); + Event event = findByIdOrElseThrow(eventId); checkEventState(event); return event.toDto(); } - @Secured("ROLE_ADMIN") + @Secured("ADMIN") public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = eventRepository.findById(eventId) - .orElseThrow(() -> new IllegalArgumentException("Event not found")); + Event event = findByIdOrElseThrow(eventId); event.update(eventRequestDto); @@ -86,10 +86,20 @@ private void checkEventState(Event event) { } } - public boolean isValidEvent(Event event) { - LocalDateTime now = LocalDateTime.now(); - return ((event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && - (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) - ? true : false; + public Event getValidEventOrThrow(Long eventId) { + Event event = findByIdOrElseThrow(eventId); + + checkEventState(event); + + if(event.getState() != EventState.VALID) { + throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); + } + + return event; + } + + public Event findByIdOrElseThrow(Long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); } } diff --git a/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java index ea339d6..0204fb3 100644 --- a/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java +++ b/src/main/java/com/example/eightyage/global/dto/ValidationMessage.java @@ -12,5 +12,10 @@ public final class ValidationMessage { public static final String NOT_BLANK_PASSWORD = "비밀번호는 필수 입력 값입니다."; public static final String PATTERN_PASSWORD = "비밀번호는 영어, 숫자 포함 8자리 이상이어야 합니다."; public static final String PATTERN_PASSWORD_REGEXP = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$"; + public static final String NOT_BLANK_EVENT_NAME = "이벤트 이름은 필수 입력 값입니다."; + public static final String NOT_BLANK_EVENT_DESCRIPTION = "이벤트 설명은 필수 입력 값입니다."; + public static final String INVALID_EVENT_QUANTITY = "수량은 1개 이상이어야 합니다."; + public static final String NOT_NULL_START_DATE = "시작 날짜는 필수 입력 값입니다."; + public static final String NOT_NULL_END_DATE = "종료 날짜는 필수 입력 값입니다."; } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java index a3adf1d..f319ae2 100644 --- a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java +++ b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java @@ -24,7 +24,15 @@ public enum ErrorMessage { INVALID_JWT_SIGNATURE("유효하지 않는 JWT 서명입니다."), EXPIRED_JWT_TOKEN("만료된 JWT 토큰입니다."), - UNSUPPORTED_JWT_TOKEN("지원되지 않는 JWT 토큰입니다."); + UNSUPPORTED_JWT_TOKEN("지원되지 않는 JWT 토큰입니다."), + + EVENT_NOT_FOUND("이벤트를 찾을 수 없습니다."), + INVALID_EVENT_PERIOD("이벤트 기간이 아닙니다."), + COUPON_ALREADY_ISSUED("이미 쿠폰 발급 받은 사용자입니다."), + COUPON_OUT_OF_STOCK("쿠폰 수량이 소진되었습니다."), + COUPON_NOT_FOUND("쿠폰을 찾을 수 없습니다."), + COUPON_ALREADY_USED("이미 사용된 쿠폰입니다."), + COUPON_FORBIDDEN("본인의 쿠폰이 아닙니다."); private final String message; From d0f485b9dc3af6ff7b9df1594197fad72918d35e Mon Sep 17 00:00:00 2001 From: queenriwon Date: Thu, 27 Mar 2025 15:44:23 +0900 Subject: [PATCH 097/164] =?UTF-8?q?test(user):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20=EB=8D=94=EB=AF=B8=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - deleted_at과 email에 대한 인덱스를 지정하고 싶어 랜덤 삭제 더미데이터 생성코드 작성 --- .../user/repository/UserBulkRepository.java | 31 ++++++++++ .../example/eightyage/data/UserBulkTest.java | 56 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java create mode 100644 src/test/java/com/example/eightyage/data/UserBulkTest.java diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java new file mode 100644 index 0000000..2a861c0 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java @@ -0,0 +1,31 @@ +package com.example.eightyage.domain.user.repository; + +import com.example.eightyage.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Random; + +@Repository +@RequiredArgsConstructor +public class UserBulkRepository { + + private final JdbcTemplate jdbcTemplate; + private final int BATCH_SIZE = 1000; + + public void bulkInsertUsers(List users) { + String sql = "INSERT INTO user (email, password, nickname, deleted_at) values (?, ?, ?, ?)"; + + Random random = new Random(); + + jdbcTemplate.batchUpdate(sql, users, BATCH_SIZE, (ps, argument) -> { + ps.setString(1, argument.getEmail()); + ps.setString(2, argument.getPassword()); + ps.setString(3, argument.getNickname()); + ps.setString(4, random.nextBoolean() ? null : LocalDateTime.now().toString()); // 랜덤으로 유저 삭제 + }); + } +} diff --git a/src/test/java/com/example/eightyage/data/UserBulkTest.java b/src/test/java/com/example/eightyage/data/UserBulkTest.java new file mode 100644 index 0000000..0e58d7d --- /dev/null +++ b/src/test/java/com/example/eightyage/data/UserBulkTest.java @@ -0,0 +1,56 @@ +package com.example.eightyage.data; + +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile(value = "test") +public class UserBulkTest { + + @Autowired + private UserBulkRepository userBulkRepository; + + @Test + void 유저_데이터_백만건_생성() { + + List batchList = new ArrayList<>(); + + for (int i = 0; i < 1_000_000; i++) { + User user = User.builder() + .email(i + "@email.com") + .nickname("nickname" + i) + .password("password") + .userRole(UserRole.ROLE_USER) + .build(); + batchList.add(user); + + if (batchList.size() == 1000) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + +// sleep(500); + } + } + + if (!batchList.isEmpty()) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} From b883239e27bc5a3db36f97fd427fab53daccbff3 Mon Sep 17 00:00:00 2001 From: peridot Date: Thu, 27 Mar 2025 16:01:59 +0900 Subject: [PATCH 098/164] =?UTF-8?q?fix(coupon):=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20=EB=B0=98=EC=98=81=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/service/CouponService.java | 4 +++- .../domain/event/controller/EventController.java | 5 +++-- .../eightyage/domain/event/entity/Event.java | 9 +++++++++ .../domain/event/service/EventService.java | 15 +++++---------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 81a2d66..1cfd7da 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -27,9 +27,11 @@ public class CouponService { private final EventService eventService; private final StringRedisTemplate stringRedisTemplate; + private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; + public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { // 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); + Long remain = stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); if (remain == null || remain < 0) { // atomic? `DESC`? throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); } diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 7c12b97..9a38ff6 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.event.dto.request.EventRequestDto; import com.example.eightyage.domain.event.dto.response.EventResponseDto; import com.example.eightyage.domain.event.service.EventService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -16,7 +17,7 @@ public class EventController { private final EventService eventService; @PostMapping("/v1/events") - public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { + public ResponseEntity createEvent(@Valid @RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); } @@ -31,7 +32,7 @@ public ResponseEntity getEvent(@PathVariable long eventId) { } @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { + public ResponseEntity updateEvent(@PathVariable long eventId, @Valid @RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); } } diff --git a/src/main/java/com/example/eightyage/domain/event/entity/Event.java b/src/main/java/com/example/eightyage/domain/event/entity/Event.java index 4f71301..7b2b560 100644 --- a/src/main/java/com/example/eightyage/domain/event/entity/Event.java +++ b/src/main/java/com/example/eightyage/domain/event/entity/Event.java @@ -60,4 +60,13 @@ public void update(EventRequestDto eventRequestDto) { this.startDate = eventRequestDto.getStartDate(); this.endDate = eventRequestDto.getEndDate(); } + + public boolean isValidAt(LocalDateTime time) { + return (startDate.isBefore(time) || startDate.isEqual(time)) && (endDate.isAfter(time) || endDate.isEqual(time)); + } + + public void updateStateAt(LocalDateTime time) { + EventState newState = isValidAt(time) ? EventState.VALID : EventState.INVALID; + this.state = newState; + } } diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index a96f6e1..9af0bfc 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -73,15 +73,10 @@ public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDt } private void checkEventState(Event event) { - LocalDateTime now = LocalDateTime.now(); - EventState newState = - ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && - (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) - ? EventState.VALID - : EventState.INVALID; - - if (event.getState() != newState) { - event.setState(newState); + EventState prevState = event.getState(); + event.updateStateAt(LocalDateTime.now()); + + if(event.getState() != prevState) { eventRepository.save(event); } } @@ -89,7 +84,7 @@ private void checkEventState(Event event) { public Event getValidEventOrThrow(Long eventId) { Event event = findByIdOrElseThrow(eventId); - checkEventState(event); + event.updateStateAt(LocalDateTime.now()); if(event.getState() != EventState.VALID) { throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); From 460bc80377cebdbcf7d8db956d354c2dacd9e79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:08:40 +0900 Subject: [PATCH 099/164] =?UTF-8?q?chore:=20application.yml=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../eightyage/global/config/EnvConfig.java | 18 ++++++++++++++++++ src/main/resources/application.yml | 13 +++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/main/java/com/example/eightyage/global/config/EnvConfig.java diff --git a/build.gradle b/build.gradle index f55cac0..7ef2fa9 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,9 @@ dependencies { // spring cloud AWS S3 implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' + + // env + implementation 'io.github.cdimascio:java-dotenv:5.2.2' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/global/config/EnvConfig.java b/src/main/java/com/example/eightyage/global/config/EnvConfig.java new file mode 100644 index 0000000..836cde9 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/EnvConfig.java @@ -0,0 +1,18 @@ +package com.example.eightyage.global.config; + +import io.github.cdimascio.dotenv.Dotenv; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +@Configuration +public class EnvConfig { + @PostConstruct + public void init() { + Dotenv dotenv = Dotenv.load(); + + System.setProperty("DB_URL", dotenv.get("DB_URL")); + System.setProperty("DB_USER", dotenv.get("DB_USER")); + System.setProperty("DB_PASSWORD", dotenv.get("DB_PASSWORD")); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e592d69..9be0ae7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,6 +10,9 @@ server: timeout: 1800 spring: + config: + import: optional:file:.env[.properties] + application: name: eightyage @@ -29,6 +32,16 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect +aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: ap-northeast-2 + s3: + bucket: my-gom-bucket + jwt: secret: key: ${JWT_SECRET_KEY} + + From 27b2de1ea41d826b48be5788f5f9a850ab9d948b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:09:17 +0900 Subject: [PATCH 100/164] =?UTF-8?q?chore:=20application.yml=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9be0ae7..ffef306 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,6 +32,16 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: + static: ap-northeast-2 + s3: + bucket: my-gom-bucket + aws: credentials: access-key: ${AWS_ACCESS_KEY} From 40518fdef4a6366059019817bdd01adbc8ab16b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:21:33 +0900 Subject: [PATCH 101/164] =?UTF-8?q?chore:=20AWS,=20JWT=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20system=20properties=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 --- .../java/com/example/eightyage/global/config/EnvConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/example/eightyage/global/config/EnvConfig.java b/src/main/java/com/example/eightyage/global/config/EnvConfig.java index 836cde9..b453bbd 100644 --- a/src/main/java/com/example/eightyage/global/config/EnvConfig.java +++ b/src/main/java/com/example/eightyage/global/config/EnvConfig.java @@ -14,5 +14,8 @@ public void init() { System.setProperty("DB_URL", dotenv.get("DB_URL")); System.setProperty("DB_USER", dotenv.get("DB_USER")); System.setProperty("DB_PASSWORD", dotenv.get("DB_PASSWORD")); + System.setProperty("DB_URL", dotenv.get("AWS_ACCESS_KEY")); + System.setProperty("DB_USER", dotenv.get("AWS_SECRET_KEY")); + System.setProperty("DB_PASSWORD", dotenv.get("JWT_SECRET_KEY")); } } From 1284c31faf901c5c08867c4c4b641dd79da1e761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:28:13 +0900 Subject: [PATCH 102/164] =?UTF-8?q?fix:=20Redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=9D=BC=EB=8B=A8=20=EC=BD=94=EB=93=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/service/CouponService.java | 152 ++++++------- .../domain/event/service/EventService.java | 210 +++++++++--------- .../eightyage/global/config/RedisConfig.java | 48 ++-- 3 files changed, 205 insertions(+), 205 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 81a2d66..2852a82 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,76 +1,76 @@ -package com.example.eightyage.domain.coupon.service; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; -import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.service.EventService; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.global.dto.AuthUser; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.exception.NotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CouponService { - - private final CouponRepository couponRepository; - private final EventService eventService; - private final StringRedisTemplate stringRedisTemplate; - - public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { - // 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); - if (remain == null || remain < 0) { // atomic? `DESC`? - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - - Event event = eventService.getValidEventOrThrow(eventId); - - if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } - - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); - - couponRepository.save(coupon); - - return coupon.toDto(); - } - - public Page getMyCoupons(AuthUser authUser, int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); - - return coupons.map(Coupon::toDto); - } - - public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = findByIdOrElseThrow(couponId); - - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); - } - - if(!coupon.getState().equals(CouponState.VALID)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); - } - - return coupon.toDto(); - } - - public Coupon findByIdOrElseThrow(Long couponId) { - return couponRepository.findById(couponId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.coupon.service; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.entity.Coupon; +//import com.example.eightyage.domain.coupon.entity.CouponState; +//import com.example.eightyage.domain.coupon.repository.CouponRepository; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.service.EventService; +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.global.dto.AuthUser; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import com.example.eightyage.global.exception.ForbiddenException; +//import com.example.eightyage.global.exception.NotFoundException; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.stereotype.Service; +// +//@Service +//@RequiredArgsConstructor +//public class CouponService { +// +// private final CouponRepository couponRepository; +// private final EventService eventService; +// private final StringRedisTemplate stringRedisTemplate; +// +// public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { +// // 수량 우선 차감 +// Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); +// if (remain == null || remain < 0) { // atomic? `DESC`? +// throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); +// } +// +// Event event = eventService.getValidEventOrThrow(eventId); +// +// if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); +// } +// +// // 쿠폰 발급 및 저장 +// Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); +// +// couponRepository.save(coupon); +// +// return coupon.toDto(); +// } +// +// public Page getMyCoupons(AuthUser authUser, int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); +// +// return coupons.map(Coupon::toDto); +// } +// +// public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { +// Coupon coupon = findByIdOrElseThrow(couponId); +// +// if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { +// throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); +// } +// +// if(!coupon.getState().equals(CouponState.VALID)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); +// } +// +// return coupon.toDto(); +// } +// +// public Coupon findByIdOrElseThrow(Long couponId) { +// return couponRepository.findById(couponId) +// .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index a96f6e1..539a7d8 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -1,105 +1,105 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.entity.EventState; -import com.example.eightyage.domain.event.repository.EventRepository; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - -@Service -@RequiredArgsConstructor -public class EventService { - - private final EventRepository eventRepository; - private final StringRedisTemplate stringRedisTemplate; - - @Secured("ADMIN") - public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { - Event event = new Event( - eventRequestDto.getName(), - eventRequestDto.getDescription(), - eventRequestDto.getQuantity(), - eventRequestDto.getStartDate(), - eventRequestDto.getEndDate() - ); - - checkEventState(event); - - Event savedEvent = eventRepository.save(event); - - stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); - - return savedEvent.toDto(); - } - - public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page events = eventRepository.findAll(pageable); - - // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); - - return events.map(Event::toDto); - } - - public EventResponseDto getEvent(long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - return event.toDto(); - } - - @Secured("ADMIN") - public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = findByIdOrElseThrow(eventId); - - event.update(eventRequestDto); - - checkEventState(event); - - return event.toDto(); - } - - private void checkEventState(Event event) { - LocalDateTime now = LocalDateTime.now(); - EventState newState = - ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && - (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) - ? EventState.VALID - : EventState.INVALID; - - if (event.getState() != newState) { - event.setState(newState); - eventRepository.save(event); - } - } - - public Event getValidEventOrThrow(Long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - if(event.getState() != EventState.VALID) { - throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); - } - - return event; - } - - public Event findByIdOrElseThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.event.service; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.entity.EventState; +//import com.example.eightyage.domain.event.repository.EventRepository; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.security.access.annotation.Secured; +//import org.springframework.stereotype.Service; +// +//import java.time.LocalDateTime; +// +//@Service +//@RequiredArgsConstructor +//public class EventService { +// +// private final EventRepository eventRepository; +// private final StringRedisTemplate stringRedisTemplate; +// +// @Secured("ADMIN") +// public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { +// Event event = new Event( +// eventRequestDto.getName(), +// eventRequestDto.getDescription(), +// eventRequestDto.getQuantity(), +// eventRequestDto.getStartDate(), +// eventRequestDto.getEndDate() +// ); +// +// checkEventState(event); +// +// Event savedEvent = eventRepository.save(event); +// +// stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); +// +// return savedEvent.toDto(); +// } +// +// public Page getEvents(int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page events = eventRepository.findAll(pageable); +// +// // 모든 events들 checkState로 state 상태 갱신하기 +// events.forEach(this::checkEventState); +// +// return events.map(Event::toDto); +// } +// +// public EventResponseDto getEvent(long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// @Secured("ADMIN") +// public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { +// Event event = findByIdOrElseThrow(eventId); +// +// event.update(eventRequestDto); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// private void checkEventState(Event event) { +// LocalDateTime now = LocalDateTime.now(); +// EventState newState = +// ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && +// (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) +// ? EventState.VALID +// : EventState.INVALID; +// +// if (event.getState() != newState) { +// event.setState(newState); +// eventRepository.save(event); +// } +// } +// +// public Event getValidEventOrThrow(Long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// if(event.getState() != EventState.VALID) { +// throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); +// } +// +// return event; +// } +// +// public Event findByIdOrElseThrow(Long eventId) { +// return eventRepository.findById(eventId) +// .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/global/config/RedisConfig.java b/src/main/java/com/example/eightyage/global/config/RedisConfig.java index 8373a4a..c4174ae 100644 --- a/src/main/java/com/example/eightyage/global/config/RedisConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedisConfig.java @@ -1,26 +1,26 @@ package com.example.eightyage.global.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - return new StringRedisTemplate(redisConnectionFactory); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - return redisTemplate; - } -} +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.redis.connection.RedisConnectionFactory; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +//import org.springframework.data.redis.serializer.StringRedisSerializer; +// +//@Configuration +//public class RedisConfig { +// @Bean +// public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { +// return new StringRedisTemplate(redisConnectionFactory); +// } +// +// @Bean +// public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { +// RedisTemplate redisTemplate = new RedisTemplate<>(); +// redisTemplate.setConnectionFactory(redisConnectionFactory); +// redisTemplate.setKeySerializer(new StringRedisSerializer()); +// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); +// return redisTemplate; +// } +//} From 1e356c36e521850b489896c1e703b67ca32bdc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:32:36 +0900 Subject: [PATCH 103/164] =?UTF-8?q?fix:=20Redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=9D=BC=EB=8B=A8=20=EC=BD=94=EB=93=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 76 +++++++++---------- .../event/controller/EventController.java | 74 +++++++++--------- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index a57223a..c1e1939 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,38 +1,38 @@ -package com.example.eightyage.domain.coupon.controller; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.service.CouponService; -import com.example.eightyage.global.dto.AuthUser; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class CouponController { - - private final CouponService couponService; - - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); - } - - @GetMapping("/v1/coupons/my") - public ResponseEntity> getMyCoupons( - @AuthenticationPrincipal AuthUser authUser, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); - } - - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { - return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); - } -} +//package com.example.eightyage.domain.coupon.controller; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.service.CouponService; +//import com.example.eightyage.global.dto.AuthUser; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.security.core.annotation.AuthenticationPrincipal; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class CouponController { +// +// private final CouponService couponService; +// +// @PostMapping("/v1/events/{eventId}/coupons") +// public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { +// return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); +// } +// +// @GetMapping("/v1/coupons/my") +// public ResponseEntity> getMyCoupons( +// @AuthenticationPrincipal AuthUser authUser, +// @RequestParam(defaultValue = "1") int page, +// @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); +// } +// +// @GetMapping("/v1/coupons/{couponId}") +// public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { +// return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 7c12b97..3d3104c 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -1,37 +1,37 @@ -package com.example.eightyage.domain.event.controller; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.service.EventService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class EventController { - - private final EventService eventService; - - @PostMapping("/v1/events") - public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); - } - - @GetMapping("/v1/events") - public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(eventService.getEvents(page, size)); - } - - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getEvent(@PathVariable long eventId) { - return ResponseEntity.ok(eventService.getEvent(eventId)); - } - - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); - } -} +//package com.example.eightyage.domain.event.controller; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.service.EventService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class EventController { +// +// private final EventService eventService; +// +// @PostMapping("/v1/events") +// public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); +// } +// +// @GetMapping("/v1/events") +// public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(eventService.getEvents(page, size)); +// } +// +// @GetMapping("/v1/events/{eventId}") +// public ResponseEntity getEvent(@PathVariable long eventId) { +// return ResponseEntity.ok(eventService.getEvent(eventId)); +// } +// +// @PatchMapping("/v1/events/{eventId}") +// public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); +// } +//} From c5b5c1746c449fc9808a0efca30e92c393dadb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:39:16 +0900 Subject: [PATCH 104/164] =?UTF-8?q?fix:=20redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EC=BD=94=EB=93=9C=20=EC=A3=BC=EC=84=9D=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 76 +++---- .../domain/coupon/service/CouponService.java | 152 ++++++------- .../event/controller/EventController.java | 74 +++--- .../domain/event/service/EventService.java | 210 +++++++++--------- .../eightyage/global/config/RedisConfig.java | 52 ++--- 5 files changed, 282 insertions(+), 282 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index a57223a..c1e1939 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,38 +1,38 @@ -package com.example.eightyage.domain.coupon.controller; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.service.CouponService; -import com.example.eightyage.global.dto.AuthUser; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class CouponController { - - private final CouponService couponService; - - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); - } - - @GetMapping("/v1/coupons/my") - public ResponseEntity> getMyCoupons( - @AuthenticationPrincipal AuthUser authUser, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); - } - - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { - return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); - } -} +//package com.example.eightyage.domain.coupon.controller; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.service.CouponService; +//import com.example.eightyage.global.dto.AuthUser; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.security.core.annotation.AuthenticationPrincipal; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class CouponController { +// +// private final CouponService couponService; +// +// @PostMapping("/v1/events/{eventId}/coupons") +// public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { +// return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); +// } +// +// @GetMapping("/v1/coupons/my") +// public ResponseEntity> getMyCoupons( +// @AuthenticationPrincipal AuthUser authUser, +// @RequestParam(defaultValue = "1") int page, +// @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); +// } +// +// @GetMapping("/v1/coupons/{couponId}") +// public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { +// return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 81a2d66..2852a82 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,76 +1,76 @@ -package com.example.eightyage.domain.coupon.service; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; -import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.service.EventService; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.global.dto.AuthUser; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.exception.NotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CouponService { - - private final CouponRepository couponRepository; - private final EventService eventService; - private final StringRedisTemplate stringRedisTemplate; - - public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { - // 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); - if (remain == null || remain < 0) { // atomic? `DESC`? - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - - Event event = eventService.getValidEventOrThrow(eventId); - - if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } - - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); - - couponRepository.save(coupon); - - return coupon.toDto(); - } - - public Page getMyCoupons(AuthUser authUser, int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); - - return coupons.map(Coupon::toDto); - } - - public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = findByIdOrElseThrow(couponId); - - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); - } - - if(!coupon.getState().equals(CouponState.VALID)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); - } - - return coupon.toDto(); - } - - public Coupon findByIdOrElseThrow(Long couponId) { - return couponRepository.findById(couponId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.coupon.service; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.entity.Coupon; +//import com.example.eightyage.domain.coupon.entity.CouponState; +//import com.example.eightyage.domain.coupon.repository.CouponRepository; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.service.EventService; +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.global.dto.AuthUser; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import com.example.eightyage.global.exception.ForbiddenException; +//import com.example.eightyage.global.exception.NotFoundException; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.stereotype.Service; +// +//@Service +//@RequiredArgsConstructor +//public class CouponService { +// +// private final CouponRepository couponRepository; +// private final EventService eventService; +// private final StringRedisTemplate stringRedisTemplate; +// +// public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { +// // 수량 우선 차감 +// Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); +// if (remain == null || remain < 0) { // atomic? `DESC`? +// throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); +// } +// +// Event event = eventService.getValidEventOrThrow(eventId); +// +// if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); +// } +// +// // 쿠폰 발급 및 저장 +// Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); +// +// couponRepository.save(coupon); +// +// return coupon.toDto(); +// } +// +// public Page getMyCoupons(AuthUser authUser, int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); +// +// return coupons.map(Coupon::toDto); +// } +// +// public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { +// Coupon coupon = findByIdOrElseThrow(couponId); +// +// if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { +// throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); +// } +// +// if(!coupon.getState().equals(CouponState.VALID)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); +// } +// +// return coupon.toDto(); +// } +// +// public Coupon findByIdOrElseThrow(Long couponId) { +// return couponRepository.findById(couponId) +// .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 7c12b97..3d3104c 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -1,37 +1,37 @@ -package com.example.eightyage.domain.event.controller; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.service.EventService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class EventController { - - private final EventService eventService; - - @PostMapping("/v1/events") - public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); - } - - @GetMapping("/v1/events") - public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(eventService.getEvents(page, size)); - } - - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getEvent(@PathVariable long eventId) { - return ResponseEntity.ok(eventService.getEvent(eventId)); - } - - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); - } -} +//package com.example.eightyage.domain.event.controller; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.service.EventService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class EventController { +// +// private final EventService eventService; +// +// @PostMapping("/v1/events") +// public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); +// } +// +// @GetMapping("/v1/events") +// public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(eventService.getEvents(page, size)); +// } +// +// @GetMapping("/v1/events/{eventId}") +// public ResponseEntity getEvent(@PathVariable long eventId) { +// return ResponseEntity.ok(eventService.getEvent(eventId)); +// } +// +// @PatchMapping("/v1/events/{eventId}") +// public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index a96f6e1..539a7d8 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -1,105 +1,105 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.entity.EventState; -import com.example.eightyage.domain.event.repository.EventRepository; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - -@Service -@RequiredArgsConstructor -public class EventService { - - private final EventRepository eventRepository; - private final StringRedisTemplate stringRedisTemplate; - - @Secured("ADMIN") - public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { - Event event = new Event( - eventRequestDto.getName(), - eventRequestDto.getDescription(), - eventRequestDto.getQuantity(), - eventRequestDto.getStartDate(), - eventRequestDto.getEndDate() - ); - - checkEventState(event); - - Event savedEvent = eventRepository.save(event); - - stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); - - return savedEvent.toDto(); - } - - public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page events = eventRepository.findAll(pageable); - - // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); - - return events.map(Event::toDto); - } - - public EventResponseDto getEvent(long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - return event.toDto(); - } - - @Secured("ADMIN") - public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = findByIdOrElseThrow(eventId); - - event.update(eventRequestDto); - - checkEventState(event); - - return event.toDto(); - } - - private void checkEventState(Event event) { - LocalDateTime now = LocalDateTime.now(); - EventState newState = - ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && - (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) - ? EventState.VALID - : EventState.INVALID; - - if (event.getState() != newState) { - event.setState(newState); - eventRepository.save(event); - } - } - - public Event getValidEventOrThrow(Long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - if(event.getState() != EventState.VALID) { - throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); - } - - return event; - } - - public Event findByIdOrElseThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.event.service; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.entity.EventState; +//import com.example.eightyage.domain.event.repository.EventRepository; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.security.access.annotation.Secured; +//import org.springframework.stereotype.Service; +// +//import java.time.LocalDateTime; +// +//@Service +//@RequiredArgsConstructor +//public class EventService { +// +// private final EventRepository eventRepository; +// private final StringRedisTemplate stringRedisTemplate; +// +// @Secured("ADMIN") +// public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { +// Event event = new Event( +// eventRequestDto.getName(), +// eventRequestDto.getDescription(), +// eventRequestDto.getQuantity(), +// eventRequestDto.getStartDate(), +// eventRequestDto.getEndDate() +// ); +// +// checkEventState(event); +// +// Event savedEvent = eventRepository.save(event); +// +// stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); +// +// return savedEvent.toDto(); +// } +// +// public Page getEvents(int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page events = eventRepository.findAll(pageable); +// +// // 모든 events들 checkState로 state 상태 갱신하기 +// events.forEach(this::checkEventState); +// +// return events.map(Event::toDto); +// } +// +// public EventResponseDto getEvent(long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// @Secured("ADMIN") +// public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { +// Event event = findByIdOrElseThrow(eventId); +// +// event.update(eventRequestDto); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// private void checkEventState(Event event) { +// LocalDateTime now = LocalDateTime.now(); +// EventState newState = +// ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && +// (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) +// ? EventState.VALID +// : EventState.INVALID; +// +// if (event.getState() != newState) { +// event.setState(newState); +// eventRepository.save(event); +// } +// } +// +// public Event getValidEventOrThrow(Long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// if(event.getState() != EventState.VALID) { +// throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); +// } +// +// return event; +// } +// +// public Event findByIdOrElseThrow(Long eventId) { +// return eventRepository.findById(eventId) +// .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/global/config/RedisConfig.java b/src/main/java/com/example/eightyage/global/config/RedisConfig.java index 8373a4a..8eed9c0 100644 --- a/src/main/java/com/example/eightyage/global/config/RedisConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedisConfig.java @@ -1,26 +1,26 @@ -package com.example.eightyage.global.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - return new StringRedisTemplate(redisConnectionFactory); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - return redisTemplate; - } -} +//package com.example.eightyage.global.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.redis.connection.RedisConnectionFactory; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +//import org.springframework.data.redis.serializer.StringRedisSerializer; +// +//@Configuration +//public class RedisConfig { +// @Bean +// public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { +// return new StringRedisTemplate(redisConnectionFactory); +// } +// +// @Bean +// public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { +// RedisTemplate redisTemplate = new RedisTemplate<>(); +// redisTemplate.setConnectionFactory(redisConnectionFactory); +// redisTemplate.setKeySerializer(new StringRedisSerializer()); +// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); +// return redisTemplate; +// } +//} From f83b5197206a60af2503b0d6da9b203ac90cdd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:58:29 +0900 Subject: [PATCH 105/164] =?UTF-8?q?refactor(product):=20reauestDto=20?= =?UTF-8?q?=EB=A5=BC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=8B=A8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 4 ++-- .../domain/product/service/ProductService.java | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index bcf8666..5507b88 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -24,7 +24,7 @@ public class ProductController { @Secured("ROLE_ADMIN") @PostMapping("/v1/products") public ResponseEntity saveProduct(@Valid @RequestBody ProductSaveRequestDto requestDto){ - ProductSaveResponseDto responseDto = productService.saveProduct(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice()); + ProductSaveResponseDto responseDto = productService.saveProduct(requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -36,7 +36,7 @@ public ResponseEntity updateProduct( @PathVariable Long productId, @RequestBody ProductUpdateRequestDto requestDto ){ - ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getSaleState(), requestDto.getPrice()); + ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto); return ResponseEntity.ok(responseDto); } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 52be317..fe42006 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -1,5 +1,7 @@ package com.example.eightyage.domain.product.service; +import com.example.eightyage.domain.product.dto.request.ProductSaveRequestDto; +import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; @@ -32,8 +34,8 @@ public class ProductService { // 제품 생성 @Transactional - public ProductSaveResponseDto saveProduct(String productName, Category category, String content, Integer price) { - Product product = new Product(productName, category, content, price, SaleState.FOR_SALE); + public ProductSaveResponseDto saveProduct(ProductSaveRequestDto requestDto) { + Product product = new Product(requestDto.getProductName(), requestDto.getCategory(), requestDto.getContent(), requestDto.getPrice(), SaleState.FOR_SALE); Product savedProduct = productRepository.save(product); @@ -50,14 +52,14 @@ public ProductSaveResponseDto saveProduct(String productName, Category category, // 제품 수정 @Transactional - public ProductUpdateResponseDto updateProduct(Long productId, String productName, Category category, String content, SaleState saleState, Integer price) { + public ProductUpdateResponseDto updateProduct(Long productId, ProductUpdateRequestDto requestDto) { Product findProduct = findProductByIdOrElseThrow(productId); - findProduct.updateName(productName); - findProduct.updateCategory(category); - findProduct.updateContent(content); - findProduct.updateSaleState(saleState); - findProduct.updatePrice(price); + findProduct.updateName(requestDto.getProductName()); + findProduct.updateCategory(requestDto.getCategory()); + findProduct.updateContent(requestDto.getContent()); + findProduct.updateSaleState(requestDto.getSaleState()); + findProduct.updatePrice(requestDto.getPrice()); return ProductUpdateResponseDto.builder() .productName(findProduct.getName()) From 747421dbbc8331ca2045f854c92289fd45f0d872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 16:58:36 +0900 Subject: [PATCH 106/164] =?UTF-8?q?refactor(review):=20reauestDto=20?= =?UTF-8?q?=EB=A5=BC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=8B=A8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/controller/ReviewController.java | 4 ++-- .../domain/review/service/ReviewService.java | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index ee0040c..bdab2e7 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -35,7 +35,7 @@ public ResponseEntity saveReview( @PathVariable Long productId, @Valid @RequestBody ReviewSaveRequestDto requestDto ){ - ReviewSaveResponseDto responseDto = reviewService.saveReview(authUser.getUserId(), productId, requestDto.getScore(), requestDto.getContent()); + ReviewSaveResponseDto responseDto = reviewService.saveReview(authUser.getUserId(), productId, requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -48,7 +48,7 @@ public ResponseEntity updateReview( @PathVariable Long reviewId, @RequestBody ReviewUpdateRequestDto requestDto ){ - ReviewUpdateResponseDto responseDto = reviewService.updateReview(authUser.getUserId(), reviewId, requestDto.getScore(), requestDto.getContent()); + ReviewUpdateResponseDto responseDto = reviewService.updateReview(authUser.getUserId(), reviewId, requestDto); return ResponseEntity.ok(responseDto); } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 2fcf099..5230b08 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -4,6 +4,8 @@ import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.product.service.ProductService; +import com.example.eightyage.domain.review.dto.request.ReviewSaveRequestDto; +import com.example.eightyage.domain.review.dto.request.ReviewUpdateRequestDto; import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; import com.example.eightyage.domain.review.dto.response.ReviewUpdateResponseDto; import com.example.eightyage.domain.review.dto.response.ReviewsGetResponseDto; @@ -33,11 +35,11 @@ public class ReviewService { // 리뷰 생성 @Transactional - public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double score, String content) { + public ReviewSaveResponseDto saveReview(Long userId, Long productId, ReviewSaveRequestDto requestDto) { User findUser = userService.findUserByIdOrElseThrow(userId); Product findProduct = productService.findProductByIdOrElseThrow(productId); - Review review = new Review(findUser, findProduct, score, content); + Review review = new Review(findUser, findProduct, requestDto.getScore(), requestDto.getContent()); Review savedReview = reviewRepository.save(review); return ReviewSaveResponseDto.builder() @@ -54,13 +56,13 @@ public ReviewSaveResponseDto saveReview(Long userId, Long productId, Double scor // 리뷰 수정 @Transactional - public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, Double score, String content) { + public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, ReviewUpdateRequestDto requestDto) { User findUser = userService.findUserByIdOrElseThrow(userId); Review findReview = findReviewByIdOrElseThrow(reviewId); if(findUser.getId().equals(findReview.getUser().getId())){ - findReview.updateScore(score); - findReview.updateContent(content); + findReview.updateScore(requestDto.getScore()); + findReview.updateContent(requestDto.getContent()); } else { throw new UnauthorizedException("리뷰를 수정할 권한이 없습니다."); } From 55c521e767f8273ad453f0d91198e3865c62feaf Mon Sep 17 00:00:00 2001 From: queenriwon Date: Thu, 27 Mar 2025 18:14:09 +0900 Subject: [PATCH 107/164] =?UTF-8?q?feat(user):=20User=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - UserRepository에는 3개의 조회쿼리 메서드가 있다. - 주로 사용되는 조회 조건은 id, email, delete_at이다. - id는 기본키기 때문에 인덱스를 따로 설정해줄 필요가 없다. - delete_at은 쿼리문이 id 랑 같이 사용되는데, id먼저 조회하고,null인지 아닌지만 판단하기 때문에 이 역시 인덱스를 설정해줄 필요는 없다. - email의 경우 유니크 값을 선정해주긴 했지만, 인덱스로 활용이 되지 않는 것을 확인할 수 있었다. - 따라서 email을 인덱스로 설정해주어 검색 쿼리 최적화를 할 수 있었다. - RefreshTokenRepository에는 1개의 조회 쿼리 메서드가 있다. - 토큰 내용으로 RefreshToken을 가져오는 쿼리를 실행한다. - refresh_token 테이블은 유저가 로그인할때마다 Write 하기 때문에 레코드 생성에 따른 인덱스를 재구조화 해야한다. - Write가 많은 테이블의 경우 인덱스에 따른 정렬이 빈번하게 일어나 오히려 인덱싱 효율을 낮춘다. - 리플레시 토큰을 조회하는 경우는 재발급 하는 경우밖에 없는데, 로그인에 비해 빈번하게 발생하지 않는다.(token rotation을 적용안하면 더더욱) - 따라서 refresh_token에는 인덱스를 적용하지 않는 것으로 결정했다. --- .../java/com/example/eightyage/domain/user/entity/User.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index f208901..ba56fe1 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -13,6 +13,9 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + indexes = @Index(name = "idx_email", columnList = "email") +) public class User extends TimeStamped { @Id From 8a00ef72ea691fc29c2b7cceae1e6e52409228fe Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Thu, 27 Mar 2025 19:48:22 +0900 Subject: [PATCH 108/164] =?UTF-8?q?refactor(search):=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=A4=80=20=EB=B6=80=EB=B6=84=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductService.java | 9 +++-- .../search/controller/SearchController.java | 36 +++++++++++++++++++ .../service/KeywordCountFlushService.java | 11 +++--- .../search/service/PopularKeywordService.java | 9 +++++ .../v1}/PopularKeywordServiceV1.java | 13 ++++--- .../v1}/SearchServiceV1.java | 6 +++- .../v2}/PopularKeywordServiceV2.java | 14 +++++--- .../v2}/SearchServiceV2.java | 17 ++++++--- .../v1/controller/SearchControllerV1.java | 26 -------------- .../v2/controller/SearchControllerV2.java | 27 -------------- 10 files changed, 91 insertions(+), 77 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/search/controller/SearchController.java rename src/main/java/com/example/eightyage/domain/search/{v2 => }/service/KeywordCountFlushService.java (82%) create mode 100644 src/main/java/com/example/eightyage/domain/search/service/PopularKeywordService.java rename src/main/java/com/example/eightyage/domain/search/{v1/service => service/v1}/PopularKeywordServiceV1.java (64%) rename src/main/java/com/example/eightyage/domain/search/{v1/service => service/v1}/SearchServiceV1.java (66%) rename src/main/java/com/example/eightyage/domain/search/{v2/service => service/v2}/PopularKeywordServiceV2.java (66%) rename src/main/java/com/example/eightyage/domain/search/{v2/service => service/v2}/SearchServiceV2.java (75%) delete mode 100644 src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index ffb867c..55091ef 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -10,15 +10,14 @@ import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; -import com.example.eightyage.domain.search.v1.service.SearchServiceV1; -import com.example.eightyage.domain.search.v2.service.SearchServiceV2; +import com.example.eightyage.domain.search.service.v1.SearchServiceV1; +import com.example.eightyage.domain.search.service.v2.SearchServiceV2; import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -91,7 +90,7 @@ public ProductGetResponseDto findProductById(Long productId) { } // 제품 다건 조회 version 1 - @Transactional(propagation = Propagation.REQUIRES_NEW) + @Transactional(readOnly = true) public Page getProductsV1(String productName, Category category, int size, int page) { int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); @@ -105,7 +104,7 @@ public Page getProductsV1(String productName, Category } // 제품 다건 조회 version 2 - @Transactional(propagation = Propagation.REQUIRES_NEW) + @Transactional(readOnly = true) public Page getProductsV2(String productName, Category category, int size, int page) { int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); diff --git a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java new file mode 100644 index 0000000..3bfbcc2 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.search.controller; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import com.example.eightyage.domain.search.service.v1.PopularKeywordServiceV1; +import com.example.eightyage.domain.search.service.v2.PopularKeywordServiceV2; +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 java.util.List; + +@RestController +@RequiredArgsConstructor +public class SearchController { + + private final PopularKeywordServiceV1 popularKeywordServiceV1; + private final PopularKeywordServiceV2 popularKeywordServiceV2; + + // 인기 검색어 조회 (캐시 X) + @GetMapping("/api/v1/search/popular") + public ResponseEntity> searchPopularKeywords( + @RequestParam(defaultValue = "7") int days + ) { + return ResponseEntity.ok(popularKeywordServiceV1.searchPopularKeywords(days)); + } + + // 인기 검색어 조회 (캐시 O) + @GetMapping("/api/v2/search/popular") + public ResponseEntity> searchPopularKeywordsV2( + @RequestParam(defaultValue = "7") int days + ) { + return ResponseEntity.ok(popularKeywordServiceV2.searchPopularKeywords(days)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java similarity index 82% rename from src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java rename to src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java index 2bb34b0..a81555d 100644 --- a/src/main/java/com/example/eightyage/domain/search/v2/service/KeywordCountFlushService.java +++ b/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java @@ -1,5 +1,6 @@ -package com.example.eightyage.domain.search.v2.service; +package com.example.eightyage.domain.search.service; +import com.example.eightyage.domain.search.entity.KeywordCount; import com.example.eightyage.domain.search.repository.KeywordCountRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,12 +19,14 @@ public class KeywordCountFlushService { private final CacheManager cacheManager; private final KeywordCountRepository keywordCountRepository; + private static final String KEYWORD_COUNT_MAP = "keywordCountMap"; + private static final String KEYWORD_KEY_SET = "keywordKeySet"; @Transactional @Scheduled(fixedRate = 5 * 60 * 1000) // 5분마다 실행 public void flushKeywordCounts() { - Cache countCache = cacheManager.getCache("keywordCountMap"); - Cache keySetCache = cacheManager.getCache("keywordKeySet"); + Cache countCache = cacheManager.getCache(KEYWORD_COUNT_MAP); + Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); if (countCache == null || keySetCache == null) { log.warn("캐시를 찾을 수 없습니다."); @@ -48,7 +51,7 @@ public void flushKeywordCounts() { keywordCountRepository.findById(keyword) .ifPresentOrElse( exist -> exist.updateCount(exist.getCount() + count), - () -> keywordCountRepository.save(new com.example.eightyage.domain.search.entity.KeywordCount(keyword, count)) + () -> keywordCountRepository.save(new KeywordCount(keyword, count)) ); flushed++; countCache.evict(keyword); diff --git a/src/main/java/com/example/eightyage/domain/search/service/PopularKeywordService.java b/src/main/java/com/example/eightyage/domain/search/service/PopularKeywordService.java new file mode 100644 index 0000000..2118546 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/PopularKeywordService.java @@ -0,0 +1,9 @@ +package com.example.eightyage.domain.search.service; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; + +import java.util.List; + +public interface PopularKeywordService { + List searchPopularKeywords(int days); +} diff --git a/src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java b/src/main/java/com/example/eightyage/domain/search/service/v1/PopularKeywordServiceV1.java similarity index 64% rename from src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java rename to src/main/java/com/example/eightyage/domain/search/service/v1/PopularKeywordServiceV1.java index a29caf6..84f9429 100644 --- a/src/main/java/com/example/eightyage/domain/search/v1/service/PopularKeywordServiceV1.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v1/PopularKeywordServiceV1.java @@ -1,7 +1,8 @@ -package com.example.eightyage.domain.search.v1.service; +package com.example.eightyage.domain.search.service.v1; import com.example.eightyage.domain.search.dto.PopularKeywordDto; import com.example.eightyage.domain.search.repository.SearchLogRepository; +import com.example.eightyage.domain.search.service.PopularKeywordService; import com.example.eightyage.global.exception.BadRequestException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,14 +13,16 @@ @Service @RequiredArgsConstructor -public class PopularKeywordServiceV1 { +public class PopularKeywordServiceV1 implements PopularKeywordService { private final SearchLogRepository searchLogRepository; + private static final int MIN_DAYS = 1; + private static final int MAX_DAYS = 365; // 캐시X 인기 검색어 조회 - @Transactional - public List searchPoplarKeywords(int days) { - if (days < 0 || days > 365) { + @Transactional(readOnly = true) + public List searchPopularKeywords(int days) { + if (days < MIN_DAYS || days > MAX_DAYS) { throw new BadRequestException("조회 기간은 1 ~ 365일 사이여야 합니다."); } LocalDateTime since = LocalDateTime.now().minusDays(days); diff --git a/src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java b/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java similarity index 66% rename from src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java rename to src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java index 436b145..3241312 100644 --- a/src/main/java/com/example/eightyage/domain/search/v1/service/SearchServiceV1.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java @@ -1,9 +1,11 @@ -package com.example.eightyage.domain.search.v1.service; +package com.example.eightyage.domain.search.service.v1; import com.example.eightyage.domain.search.entity.SearchLog; import com.example.eightyage.domain.search.repository.SearchLogRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @@ -12,6 +14,8 @@ public class SearchServiceV1 { private final SearchLogRepository searchLogRepository; + // 검색 키워드를 로그에 저장 + @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSearchLog(String keyword){ if(StringUtils.hasText(keyword)){ searchLogRepository.save(SearchLog.of(keyword)); diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java similarity index 66% rename from src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java rename to src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java index a1c7f8d..7b0be98 100644 --- a/src/main/java/com/example/eightyage/domain/search/v2/service/PopularKeywordServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java @@ -1,7 +1,8 @@ -package com.example.eightyage.domain.search.v2.service; +package com.example.eightyage.domain.search.service.v2; import com.example.eightyage.domain.search.dto.PopularKeywordDto; import com.example.eightyage.domain.search.repository.SearchLogRepository; +import com.example.eightyage.domain.search.service.PopularKeywordService; import com.example.eightyage.global.exception.BadRequestException; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; @@ -13,15 +14,18 @@ @Service @RequiredArgsConstructor -public class PopularKeywordServiceV2 { +public class PopularKeywordServiceV2 implements PopularKeywordService { private final SearchLogRepository searchLogRepository; + private static final int MIN_DAYS = 1; + private static final int MAX_DAYS = 365; + //캐시O 인기 검색어 조회 - @Transactional + @Transactional(readOnly = true) @Cacheable(value = "popularKeywords", key = "#days") - public List searchPopularKeywordsV2(int days) { - if (days < 1 || days > 365) { + public List searchPopularKeywords(int days) { + if (days < MIN_DAYS || days > MAX_DAYS) { throw new BadRequestException("조회 일 수는 1~365 사이여야 합니다."); } LocalDateTime since = LocalDateTime.now().minusDays(days); diff --git a/src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java similarity index 75% rename from src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java rename to src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index 00e0ebf..74ceb04 100644 --- a/src/main/java/com/example/eightyage/domain/search/v2/service/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.search.v2.service; +package com.example.eightyage.domain.search.service.v2; import com.example.eightyage.domain.search.entity.SearchLog; import com.example.eightyage.domain.search.repository.SearchLogRepository; @@ -6,6 +6,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -18,9 +19,12 @@ public class SearchServiceV2 { private final SearchLogRepository searchLogRepository; private final CacheManager cacheManager; + private static final String KEYWORD_COUNT_MAP = "keywordCountMap"; + private static final String KEYWORD_KEY_SET = "keywordKeySet"; + // 검색 키워드를 로그에 저장 - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSearchLog(String keyword) { if (StringUtils.hasText(keyword)) { searchLogRepository.save(SearchLog.of(keyword)); @@ -28,11 +32,11 @@ public void saveSearchLog(String keyword) { } // 검색 시 키워드 카운트 증가 + @Transactional public void increaseKeywordCount(String keyword) { if (!StringUtils.hasText(keyword)) return; - Cache countCache = cacheManager.getCache("keywordCountMap"); - Cache keySetCache = cacheManager.getCache("keywordKeySet"); + Cache countCache = cacheManager.getCache(KEYWORD_COUNT_MAP); if (countCache != null) { Long count = countCache.get(keyword, Long.class); @@ -40,6 +44,11 @@ public void increaseKeywordCount(String keyword) { countCache.put(keyword, count); } + updateKeywordSet(keyword); + } + + private void updateKeywordSet(String keyword) { + Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); if (keySetCache != null) { Set keywordSet = keySetCache.get("keywords", Set.class); if (keywordSet == null) { diff --git a/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java b/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java deleted file mode 100644 index 29b5efd..0000000 --- a/src/main/java/com/example/eightyage/domain/search/v1/controller/SearchControllerV1.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.eightyage.domain.search.v1.controller; - -import com.example.eightyage.domain.search.dto.PopularKeywordDto; -import com.example.eightyage.domain.search.v1.service.PopularKeywordServiceV1; -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 java.util.List; - -@RestController -@RequiredArgsConstructor -public class SearchControllerV1 { - - private final PopularKeywordServiceV1 popularKeywordService; - - // 인기 검색어 조회 (캐시 X) - @GetMapping("/api/v1/search/popular") - public ResponseEntity> searchPopularKeywords( - @RequestParam(defaultValue = "7") int days - ) { - return ResponseEntity.ok(popularKeywordService.searchPoplarKeywords(days)); - } -} diff --git a/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java b/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java deleted file mode 100644 index 2188f23..0000000 --- a/src/main/java/com/example/eightyage/domain/search/v2/controller/SearchControllerV2.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.eightyage.domain.search.v2.controller; - -import com.example.eightyage.domain.search.dto.PopularKeywordDto; -import com.example.eightyage.domain.search.v2.service.PopularKeywordServiceV2; -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 java.util.List; - -@RestController -@RequiredArgsConstructor -public class SearchControllerV2 { - - private final PopularKeywordServiceV2 popularKeywordService; - - // 인기 검색어 조회 (캐시 O) - @GetMapping("/api/v2/search/popular") - public ResponseEntity> searchPopularKeywordsV2( - @RequestParam(defaultValue = "7") int days - ) { - return ResponseEntity.ok(popularKeywordService.searchPopularKeywordsV2(days)); - } - -} From 114fdb328bb75e11a4d9b4047ff9d759bcb5d111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 20:26:28 +0900 Subject: [PATCH 109/164] =?UTF-8?q?fix:=20Redis=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A5=BC=20=EB=B0=A9=EC=A7=80=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EC=A3=BC=EC=84=9D=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 76 +++---- .../domain/coupon/service/CouponService.java | 152 ++++++------- .../event/controller/EventController.java | 74 +++--- .../domain/event/service/EventService.java | 210 +++++++++--------- .../eightyage/global/config/RedisConfig.java | 52 ++--- 5 files changed, 282 insertions(+), 282 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index a57223a..c1e1939 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,38 +1,38 @@ -package com.example.eightyage.domain.coupon.controller; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.service.CouponService; -import com.example.eightyage.global.dto.AuthUser; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class CouponController { - - private final CouponService couponService; - - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); - } - - @GetMapping("/v1/coupons/my") - public ResponseEntity> getMyCoupons( - @AuthenticationPrincipal AuthUser authUser, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); - } - - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { - return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); - } -} +//package com.example.eightyage.domain.coupon.controller; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.service.CouponService; +//import com.example.eightyage.global.dto.AuthUser; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.security.core.annotation.AuthenticationPrincipal; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class CouponController { +// +// private final CouponService couponService; +// +// @PostMapping("/v1/events/{eventId}/coupons") +// public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { +// return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); +// } +// +// @GetMapping("/v1/coupons/my") +// public ResponseEntity> getMyCoupons( +// @AuthenticationPrincipal AuthUser authUser, +// @RequestParam(defaultValue = "1") int page, +// @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); +// } +// +// @GetMapping("/v1/coupons/{couponId}") +// public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { +// return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 81a2d66..2852a82 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,76 +1,76 @@ -package com.example.eightyage.domain.coupon.service; - -import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; -import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.service.EventService; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.global.dto.AuthUser; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.exception.NotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CouponService { - - private final CouponRepository couponRepository; - private final EventService eventService; - private final StringRedisTemplate stringRedisTemplate; - - public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { - // 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); - if (remain == null || remain < 0) { // atomic? `DESC`? - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - - Event event = eventService.getValidEventOrThrow(eventId); - - if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } - - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); - - couponRepository.save(coupon); - - return coupon.toDto(); - } - - public Page getMyCoupons(AuthUser authUser, int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); - - return coupons.map(Coupon::toDto); - } - - public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = findByIdOrElseThrow(couponId); - - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); - } - - if(!coupon.getState().equals(CouponState.VALID)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); - } - - return coupon.toDto(); - } - - public Coupon findByIdOrElseThrow(Long couponId) { - return couponRepository.findById(couponId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.coupon.service; +// +//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +//import com.example.eightyage.domain.coupon.entity.Coupon; +//import com.example.eightyage.domain.coupon.entity.CouponState; +//import com.example.eightyage.domain.coupon.repository.CouponRepository; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.service.EventService; +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.global.dto.AuthUser; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import com.example.eightyage.global.exception.ForbiddenException; +//import com.example.eightyage.global.exception.NotFoundException; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.stereotype.Service; +// +//@Service +//@RequiredArgsConstructor +//public class CouponService { +// +// private final CouponRepository couponRepository; +// private final EventService eventService; +// private final StringRedisTemplate stringRedisTemplate; +// +// public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { +// // 수량 우선 차감 +// Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); +// if (remain == null || remain < 0) { // atomic? `DESC`? +// throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); +// } +// +// Event event = eventService.getValidEventOrThrow(eventId); +// +// if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); +// } +// +// // 쿠폰 발급 및 저장 +// Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); +// +// couponRepository.save(coupon); +// +// return coupon.toDto(); +// } +// +// public Page getMyCoupons(AuthUser authUser, int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); +// +// return coupons.map(Coupon::toDto); +// } +// +// public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { +// Coupon coupon = findByIdOrElseThrow(couponId); +// +// if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { +// throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); +// } +// +// if(!coupon.getState().equals(CouponState.VALID)) { +// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); +// } +// +// return coupon.toDto(); +// } +// +// public Coupon findByIdOrElseThrow(Long couponId) { +// return couponRepository.findById(couponId) +// .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 7c12b97..3d3104c 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -1,37 +1,37 @@ -package com.example.eightyage.domain.event.controller; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.service.EventService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class EventController { - - private final EventService eventService; - - @PostMapping("/v1/events") - public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); - } - - @GetMapping("/v1/events") - public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(eventService.getEvents(page, size)); - } - - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getEvent(@PathVariable long eventId) { - return ResponseEntity.ok(eventService.getEvent(eventId)); - } - - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); - } -} +//package com.example.eightyage.domain.event.controller; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.service.EventService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequestMapping("/api") +//@RequiredArgsConstructor +//public class EventController { +// +// private final EventService eventService; +// +// @PostMapping("/v1/events") +// public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); +// } +// +// @GetMapping("/v1/events") +// public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { +// return ResponseEntity.ok(eventService.getEvents(page, size)); +// } +// +// @GetMapping("/v1/events/{eventId}") +// public ResponseEntity getEvent(@PathVariable long eventId) { +// return ResponseEntity.ok(eventService.getEvent(eventId)); +// } +// +// @PatchMapping("/v1/events/{eventId}") +// public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { +// return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); +// } +//} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index a96f6e1..539a7d8 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -1,105 +1,105 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.entity.EventState; -import com.example.eightyage.domain.event.repository.EventRepository; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - -@Service -@RequiredArgsConstructor -public class EventService { - - private final EventRepository eventRepository; - private final StringRedisTemplate stringRedisTemplate; - - @Secured("ADMIN") - public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { - Event event = new Event( - eventRequestDto.getName(), - eventRequestDto.getDescription(), - eventRequestDto.getQuantity(), - eventRequestDto.getStartDate(), - eventRequestDto.getEndDate() - ); - - checkEventState(event); - - Event savedEvent = eventRepository.save(event); - - stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); - - return savedEvent.toDto(); - } - - public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page events = eventRepository.findAll(pageable); - - // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); - - return events.map(Event::toDto); - } - - public EventResponseDto getEvent(long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - return event.toDto(); - } - - @Secured("ADMIN") - public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = findByIdOrElseThrow(eventId); - - event.update(eventRequestDto); - - checkEventState(event); - - return event.toDto(); - } - - private void checkEventState(Event event) { - LocalDateTime now = LocalDateTime.now(); - EventState newState = - ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && - (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) - ? EventState.VALID - : EventState.INVALID; - - if (event.getState() != newState) { - event.setState(newState); - eventRepository.save(event); - } - } - - public Event getValidEventOrThrow(Long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - if(event.getState() != EventState.VALID) { - throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); - } - - return event; - } - - public Event findByIdOrElseThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); - } -} +//package com.example.eightyage.domain.event.service; +// +//import com.example.eightyage.domain.event.dto.request.EventRequestDto; +//import com.example.eightyage.domain.event.dto.response.EventResponseDto; +//import com.example.eightyage.domain.event.entity.Event; +//import com.example.eightyage.domain.event.entity.EventState; +//import com.example.eightyage.domain.event.repository.EventRepository; +//import com.example.eightyage.global.exception.BadRequestException; +//import com.example.eightyage.global.exception.ErrorMessage; +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.domain.Page; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.security.access.annotation.Secured; +//import org.springframework.stereotype.Service; +// +//import java.time.LocalDateTime; +// +//@Service +//@RequiredArgsConstructor +//public class EventService { +// +// private final EventRepository eventRepository; +// private final StringRedisTemplate stringRedisTemplate; +// +// @Secured("ADMIN") +// public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { +// Event event = new Event( +// eventRequestDto.getName(), +// eventRequestDto.getDescription(), +// eventRequestDto.getQuantity(), +// eventRequestDto.getStartDate(), +// eventRequestDto.getEndDate() +// ); +// +// checkEventState(event); +// +// Event savedEvent = eventRepository.save(event); +// +// stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); +// +// return savedEvent.toDto(); +// } +// +// public Page getEvents(int page, int size) { +// Pageable pageable = PageRequest.of(page-1, size); +// Page events = eventRepository.findAll(pageable); +// +// // 모든 events들 checkState로 state 상태 갱신하기 +// events.forEach(this::checkEventState); +// +// return events.map(Event::toDto); +// } +// +// public EventResponseDto getEvent(long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// @Secured("ADMIN") +// public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { +// Event event = findByIdOrElseThrow(eventId); +// +// event.update(eventRequestDto); +// +// checkEventState(event); +// +// return event.toDto(); +// } +// +// private void checkEventState(Event event) { +// LocalDateTime now = LocalDateTime.now(); +// EventState newState = +// ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && +// (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) +// ? EventState.VALID +// : EventState.INVALID; +// +// if (event.getState() != newState) { +// event.setState(newState); +// eventRepository.save(event); +// } +// } +// +// public Event getValidEventOrThrow(Long eventId) { +// Event event = findByIdOrElseThrow(eventId); +// +// checkEventState(event); +// +// if(event.getState() != EventState.VALID) { +// throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); +// } +// +// return event; +// } +// +// public Event findByIdOrElseThrow(Long eventId) { +// return eventRepository.findById(eventId) +// .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); +// } +//} diff --git a/src/main/java/com/example/eightyage/global/config/RedisConfig.java b/src/main/java/com/example/eightyage/global/config/RedisConfig.java index 8373a4a..8eed9c0 100644 --- a/src/main/java/com/example/eightyage/global/config/RedisConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedisConfig.java @@ -1,26 +1,26 @@ -package com.example.eightyage.global.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - return new StringRedisTemplate(redisConnectionFactory); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - return redisTemplate; - } -} +//package com.example.eightyage.global.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.redis.connection.RedisConnectionFactory; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +//import org.springframework.data.redis.serializer.StringRedisSerializer; +// +//@Configuration +//public class RedisConfig { +// @Bean +// public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { +// return new StringRedisTemplate(redisConnectionFactory); +// } +// +// @Bean +// public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { +// RedisTemplate redisTemplate = new RedisTemplate<>(); +// redisTemplate.setConnectionFactory(redisConnectionFactory); +// redisTemplate.setKeySerializer(new StringRedisSerializer()); +// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); +// return redisTemplate; +// } +//} From dfcf3356eb227e7742b29a53e389fe07c76c9685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 20:27:04 +0900 Subject: [PATCH 110/164] =?UTF-8?q?test(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=A0=9C?= =?UTF-8?q?=ED=92=88=20=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/entity/Product.java | 2 + .../domain/review/entity/Review.java | 2 + .../eightyage/domain/user/entity/User.java | 5 +- .../product/service/ProductServiceTest.java | 88 +++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index f4c64a1..e3ffaeb 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -2,6 +2,7 @@ import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -14,6 +15,7 @@ @Entity @Getter @NoArgsConstructor +@AllArgsConstructor @Table(name = "product") public class Product extends TimeStamped { diff --git a/src/main/java/com/example/eightyage/domain/review/entity/Review.java b/src/main/java/com/example/eightyage/domain/review/entity/Review.java index f198fd1..dd690cd 100644 --- a/src/main/java/com/example/eightyage/domain/review/entity/Review.java +++ b/src/main/java/com/example/eightyage/domain/review/entity/Review.java @@ -4,6 +4,7 @@ import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,6 +12,7 @@ @Entity @Getter @NoArgsConstructor +@AllArgsConstructor @Table(name = "review") public class Review extends TimeStamped { diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index f208901..fb8ff87 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -3,10 +3,7 @@ import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDateTime; diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java new file mode 100644 index 0000000..5cd2b3f --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -0,0 +1,88 @@ +package com.example.eightyage.domain.product.service; + +import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +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 java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ProductServiceTest { + + @Mock + ProductRepository productRepository; + + @Mock + ReviewRepository reviewRepository; + + @InjectMocks + ProductService productService; + + @Mock + private Product product; + + @Mock + private Review review1; + + @Mock + private Review review2; + + @Test + void 제품_단건_조회_성공(){ + // given + Long productId = 1L; + +// Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); + + given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); + + // when + ProductGetResponseDto responseDto = productService.findProductById(productId); + + // then + assertThat(responseDto.getProductName()).isEqualTo(product.getName()); + } + + @Test + void 제품_삭제_성공(){ + // given + Long productId = 1L; + + List reviewList = new ArrayList<>(); + reviewList.add(review1); + reviewList.add(review2); + + given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); + given(reviewRepository.findReviewsByProductId(any(Long.class))).willReturn(reviewList); + + // when + productService.deleteProduct(productId); + + // then + verify(review1, times(1)).delete(); + verify(review2, times(1)).delete(); + + verify(product, times(1)).delete(); + } +} \ No newline at end of file From f86fa21c02acdf3f1c78867f62844cb6b3f708f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Thu, 27 Mar 2025 21:06:57 +0900 Subject: [PATCH 111/164] =?UTF-8?q?chore:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20AWS=5FACCESS=5FKEY,=20AWS=5FSECRET=5FKEY=20?= =?UTF-8?q?=EB=A5=BC=20S3=5FACCESS=5FKEY,=20S3=5FSECRET=5FKEY=20=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7717011..44a0d39 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,8 +35,8 @@ spring: cloud: aws: credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} region: static: ap-northeast-2 s3: @@ -44,8 +44,8 @@ spring: aws: credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} region: ap-northeast-2 s3: bucket: my-gom-bucket From 7f6b901bfb4cb91111cef6ea7780683dc2183a64 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Thu, 27 Mar 2025 21:59:19 +0900 Subject: [PATCH 112/164] =?UTF-8?q?fix(search):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20readOnly?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/service/ProductService.java | 3 ++- .../domain/search/service/v2/SearchServiceV2.java | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index f8e0841..447357e 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -113,7 +113,8 @@ public Page getProductsV2(String productName, Category Page products = productRepository.findProducts(productName, category, pageable); if (StringUtils.hasText(productName) && !products.isEmpty()) { - searchServiceV2.logAndCountKeyword(productName); // 로그 저장 + 캐시 작업 + searchServiceV2.saveSearchLog(productName); // 로그 저장 + searchServiceV2.increaseKeywordCount(productName); // 캐시 작업 } return products.map(ProductSearchResponseDto::from); diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index 74ceb04..f0fd868 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -58,11 +58,4 @@ private void updateKeywordSet(String keyword) { keySetCache.put("keywords", keywordSet); } } - - @Transactional - public void logAndCountKeyword(String keyword) { - saveSearchLog(keyword); - increaseKeywordCount(keyword); - } - } From 9100e2a81766522a26082aff5ec91eb706e8716c Mon Sep 17 00:00:00 2001 From: queenriwon Date: Thu, 27 Mar 2025 23:26:42 +0900 Subject: [PATCH 113/164] =?UTF-8?q?test(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=84=B1=EB=8A=A5=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 검색 조건인 sale_state, name(UUID), category가 작성된 더미데이터 생성코드 작성 --- .../domain/product/entity/Product.java | 2 + .../repository/ProductBulkRepository.java | 32 +++++++++++ .../eightyage/bulk/ProductBulkTest.java | 56 +++++++++++++++++++ .../{data => bulk}/UserBulkTest.java | 2 +- 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java create mode 100644 src/test/java/com/example/eightyage/bulk/ProductBulkTest.java rename src/test/java/com/example/eightyage/{data => bulk}/UserBulkTest.java (97%) diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index f4c64a1..2496694 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -2,6 +2,7 @@ import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -33,6 +34,7 @@ public class Product extends TimeStamped { @Enumerated(EnumType.STRING) private SaleState saleState; + @Builder public Product(String name, Category category, String content, Integer price, SaleState saleState) { this.name = name; this.category = category; diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java new file mode 100644 index 0000000..272b3e8 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java @@ -0,0 +1,32 @@ +package com.example.eightyage.domain.product.repository; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.SaleState; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Random; + +@Repository +@RequiredArgsConstructor +public class ProductBulkRepository { + + private final JdbcTemplate jdbcTemplate; + private final int BATCH_SIZE = 50; + + public void bulkInsertProduct(List products) { + String sql = "INSERT INTO product (category, name, sale_state) values (?, ?, ?)"; + + Random random = new Random(); + + jdbcTemplate.batchUpdate(sql, products, BATCH_SIZE, (ps, argument) -> { + Category randomCategory = Category.values()[random.nextInt(Category.values().length)]; + ps.setString(1, randomCategory.name()); + ps.setString(2, argument.getName()); + ps.setString(3, random.nextBoolean() ? SaleState.FOR_SALE.name() : SaleState.SOLD_OUT.name()); + }); + } +} diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java new file mode 100644 index 0000000..6e38dc5 --- /dev/null +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -0,0 +1,56 @@ +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.repository.ProductBulkRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@SpringBootTest +@Profile(value = "test") +public class ProductBulkTest { + + @Autowired + private ProductBulkRepository productBulkRepository; + + @Test + void 제품_데이터_백만건_생성() { + + List batchList = new ArrayList<>(); + + for (int i = 0; i < 1_000; i++) { + Product product = Product.builder() + .name(UUID.randomUUID().toString()) + .build(); + batchList.add(product); + + if (batchList.size() == 50) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + +// sleep(500); + } + } + + if (!batchList.isEmpty()) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/data/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java similarity index 97% rename from src/test/java/com/example/eightyage/data/UserBulkTest.java rename to src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 0e58d7d..d599028 100644 --- a/src/test/java/com/example/eightyage/data/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,4 +1,4 @@ -package com.example.eightyage.data; +package com.example.eightyage.bulk; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.domain.user.entity.UserRole; From 92daf1a2968c381ae513b9fc30e0b284cc4cce80 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Fri, 28 Mar 2025 02:11:52 +0900 Subject: [PATCH 114/164] =?UTF-8?q?fix(search):=20Redis=EB=A1=9C=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EB=B3=80=EA=B2=BD=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/EightyageApplication.java | 2 + .../domain/search/dto/PopularKeywordDto.java | 4 ++ .../service/KeywordCountFlushService.java | 7 ++- .../search/service/v2/SearchServiceV2.java | 6 +- .../eightyage/global/config/CacheConfig.java | 60 ++++++++----------- .../eightyage/global/config/RedisConfig.java | 50 ++++++++-------- src/main/resources/application.yml | 10 ++-- 7 files changed, 69 insertions(+), 70 deletions(-) diff --git a/src/main/java/com/example/eightyage/EightyageApplication.java b/src/main/java/com/example/eightyage/EightyageApplication.java index 5b4e0a5..e4cbab4 100644 --- a/src/main/java/com/example/eightyage/EightyageApplication.java +++ b/src/main/java/com/example/eightyage/EightyageApplication.java @@ -3,11 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.scheduling.annotation.EnableScheduling; import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) @SpringBootApplication +@EnableScheduling public class EightyageApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java index dfd6507..5f7a39f 100644 --- a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -2,9 +2,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Getter @AllArgsConstructor +@NoArgsConstructor +@Setter public class PopularKeywordDto { private String keyword; diff --git a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java index a81555d..f6d3832 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java +++ b/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java @@ -45,8 +45,11 @@ public void flushKeywordCounts() { // 반복문을 이용하여 저장하기 for (String keyword : keywordSet) { - Long count = countCache.get(keyword, Long.class); - if (count == null || count == 0L) continue; + String countStr = countCache.get(keyword, String.class); + if (countStr == null) continue; + + Long count = Long.parseLong(countStr); + if (count == 0L) continue; keywordCountRepository.findById(keyword) .ifPresentOrElse( diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index f0fd868..8b9b721 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -39,9 +39,9 @@ public void increaseKeywordCount(String keyword) { Cache countCache = cacheManager.getCache(KEYWORD_COUNT_MAP); if (countCache != null) { - Long count = countCache.get(keyword, Long.class); - count = (count == null) ? 1L : count + 1; - countCache.put(keyword, count); + String countStr = countCache.get(keyword, String.class); + long count = (countStr == null) ? 1L : Long.parseLong(countStr) + 1; + countCache.put(keyword, Long.toString(count)); } updateKeywordSet(keyword); diff --git a/src/main/java/com/example/eightyage/global/config/CacheConfig.java b/src/main/java/com/example/eightyage/global/config/CacheConfig.java index 08f73ff..dc34521 100644 --- a/src/main/java/com/example/eightyage/global/config/CacheConfig.java +++ b/src/main/java/com/example/eightyage/global/config/CacheConfig.java @@ -1,51 +1,41 @@ package com.example.eightyage.global.config; -import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.caffeine.CaffeineCache; -import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; @EnableCaching @Configuration public class CacheConfig { @Bean - public CacheManager cacheManager() { - SimpleCacheManager cacheManager = new SimpleCacheManager(); - - // 키워드를 카운팅하는 캐시 - CaffeineCache keywordCountMap = new CaffeineCache( - "keywordCountMap", - Caffeine.newBuilder() - .maximumSize(10000) - .build() - ); - - // 인기 검색어를 조회하는 캐시 - CaffeineCache popularKeywords = new CaffeineCache( - "popularKeywords", - Caffeine.newBuilder() - .maximumSize(365) // days 값 기준으로 최대 365개 - .expireAfterWrite(5, TimeUnit.MINUTES) // TTL 5분 - .build() - ); - - // 현재 캐시에 저장된 키워드 목록 - CaffeineCache keywordKeySet = new CaffeineCache( - "keywordKeySet", - Caffeine.newBuilder() - .maximumSize(1) - .build() - ); - - cacheManager.setCaches(Arrays.asList(keywordCountMap, popularKeywords, keywordKeySet)); - return cacheManager; + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + // 기본 캐시 설정 + RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + // 캐시 별로 TTL 설정 + Map configMap = new HashMap<>(); + configMap.put("keywordCountMap", defaultConfig.entryTtl(Duration.ZERO)); + configMap.put("keywordKeySet", defaultConfig.entryTtl(Duration.ZERO)); + configMap.put("popularKeywords", defaultConfig.entryTtl(Duration.ofMinutes(5))); + + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(defaultConfig) + .withInitialCacheConfigurations(configMap) + .build(); } } diff --git a/src/main/java/com/example/eightyage/global/config/RedisConfig.java b/src/main/java/com/example/eightyage/global/config/RedisConfig.java index aa33f92..8373a4a 100644 --- a/src/main/java/com/example/eightyage/global/config/RedisConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedisConfig.java @@ -1,26 +1,26 @@ -//package com.example.eightyage.global.config; +package com.example.eightyage.global.config; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.data.redis.connection.RedisConnectionFactory; -//import org.springframework.data.redis.core.RedisTemplate; -//import org.springframework.data.redis.core.StringRedisTemplate; -//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -//import org.springframework.data.redis.serializer.StringRedisSerializer; -// -//@Configuration -//public class RedisConfig { -// @Bean -// public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { -// return new StringRedisTemplate(redisConnectionFactory); -// } -// -// @Bean -// public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { -// RedisTemplate redisTemplate = new RedisTemplate<>(); -// redisTemplate.setConnectionFactory(redisConnectionFactory); -// redisTemplate.setKeySerializer(new StringRedisSerializer()); -// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); -// return redisTemplate; -// } -//} +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { + return new StringRedisTemplate(redisConnectionFactory); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7717011..f3ef6e1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,11 @@ server: spring: config: import: optional:file:.env[.properties] + data: + redis: + host: localhost + port: 6379 + application: name: eightyage @@ -50,11 +55,6 @@ aws: s3: bucket: my-gom-bucket - data: - redis: - host: localhost - port: 6379 - jwt: secret: key: ${JWT_SECRET_KEY} From 7ca3ea6428c677f696b30e79be1c8a405ff9283b Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 05:11:18 +0900 Subject: [PATCH 115/164] =?UTF-8?q?feat(search):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 리뷰 및 제품 더미데이터 생성 - 제품 검색 쿼리의 최적화 확인을 위해 리뷰를 조인한 결과를 출력하도록 수정 - 인덱스는 Product에서 columnList = "saleState, category, name"로 설정 ### 수정한 것 - 리뷰의 평균을 포함하고 싶어서, 리뷰를 양방향으로 참조하도록 수정함 --- .../response/ProductSearchResponseDto.java | 31 ++++++------ .../domain/product/entity/Product.java | 15 +++--- .../product/repository/ProductRepository.java | 12 +++-- .../product/service/ProductService.java | 28 +++++++---- .../repository/ReviewBulkRepository.java | 30 +++++++++++ .../eightyage/domain/user/entity/User.java | 3 -- .../eightyage/bulk/ProductBulkTest.java | 7 +-- .../eightyage/bulk/ReviewBulkTest.java | 50 +++++++++++++++++++ .../example/eightyage/bulk/UserBulkTest.java | 2 +- 9 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java create mode 100644 src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java index 2e3c0a9..d52d526 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java @@ -1,27 +1,26 @@ package com.example.eightyage.domain.product.dto.response; -import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.Category; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder; import lombok.Getter; - -import java.time.LocalDateTime; +import lombok.NoArgsConstructor; @Builder @Getter +@NoArgsConstructor public class ProductSearchResponseDto { - private final String productName; - private final String category; - private final Integer price; - private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; + private String name; + private Category category; + private Integer price; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "0.0") + private Double scoreAvg; - public static ProductSearchResponseDto from(Product product) { - return ProductSearchResponseDto.builder() - .productName(product.getName()) - .category(product.getCategory().toString()) - .price(product.getPrice()) - .createdAt(product.getCreatedAt()) - .updatedAt(product.getModifiedAt()) - .build(); + public ProductSearchResponseDto(String name, Category category, Integer price, Double scoreAvg) { + this.name = name; + this.category = category; + this.price = price; + this.scoreAvg = scoreAvg; } } diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 2496694..169b980 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -1,21 +1,21 @@ package com.example.eightyage.domain.product.entity; +import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @NoArgsConstructor -@Table(name = "product") +@Table(name = "product", + indexes = @Index(name = "index_saleState_category_name", columnList = "saleState, category, name") +) public class Product extends TimeStamped { @Id @@ -34,6 +34,9 @@ public class Product extends TimeStamped { @Enumerated(EnumType.STRING) private SaleState saleState; + @OneToMany(mappedBy = "product") + private List reviews = new ArrayList<>(); + @Builder public Product(String name, Category category, String content, Integer price, SaleState saleState) { this.name = name; diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index da60e83..f018fd1 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.product.repository; +import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.Product; import org.springframework.data.domain.Page; @@ -17,12 +18,15 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE p.id = :productId AND p.deletedAt IS NULL") Optional findById(@Param("productId") Long productId); - @Query("SELECT p FROM Product p WHERE p.saleState = 'FOR_SALE' " + + @Query("SELECT new com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto(p.name, p.category, p.price, AVG(r.score)) " + + "FROM Product p JOIN p.reviews r " + + "WHERE p.saleState = 'FOR_SALE' " + "AND (:category IS NULL OR p.category = :category) " + "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + - "ORDER BY p.name") - Page findProducts( - @Param("name")String name, + "GROUP BY p.name, p.category, p.price " + + "ORDER BY AVG(r.score)") + Page findProductsOrderByReviewScore( + @Param("name") String name, @Param("category") Category category, Pageable pageable ); diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index f8e0841..bca531a 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -2,10 +2,7 @@ import com.example.eightyage.domain.product.dto.request.ProductSaveRequestDto; import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; -import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; -import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; -import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; -import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; +import com.example.eightyage.domain.product.dto.response.*; import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.entity.SaleState; @@ -96,13 +93,24 @@ public ProductGetResponseDto findProductById(Long productId) { public Page getProductsV1(String productName, Category category, int size, int page) { int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); - Page products = productRepository.findProducts(productName, category, pageable); + Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); - if (StringUtils.hasText(productName) && !products.isEmpty()) { + if (StringUtils.hasText(productName) && !productsResponse.isEmpty()) { searchServiceV1.saveSearchLog(productName); // 로그만 저장 } + return productsResponse; + } - return products.map(ProductSearchResponseDto::from); + // 제품 다건 조회 version 3 + @Transactional(readOnly = true) + public Page getProductsV3(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); + if (StringUtils.hasText(productName) && !productsResponse.isEmpty()) { + searchServiceV1.saveSearchLog(productName); // 로그만 저장 + } + return productsResponse; } // 제품 다건 조회 version 2 @@ -110,13 +118,13 @@ public Page getProductsV1(String productName, Category public Page getProductsV2(String productName, Category category, int size, int page) { int adjustedPage = Math.max(0, page - 1); Pageable pageable = PageRequest.of(adjustedPage, size); - Page products = productRepository.findProducts(productName, category, pageable); + Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); - if (StringUtils.hasText(productName) && !products.isEmpty()) { + if (StringUtils.hasText(productName) && !productsResponse.isEmpty()) { searchServiceV2.logAndCountKeyword(productName); // 로그 저장 + 캐시 작업 } - return products.map(ProductSearchResponseDto::from); + return productsResponse; } // 제품 삭제 diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java new file mode 100644 index 0000000..584e4bd --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java @@ -0,0 +1,30 @@ +package com.example.eightyage.domain.review.repository; + +import com.example.eightyage.domain.review.entity.Review; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Random; + +@Repository +@RequiredArgsConstructor +public class ReviewBulkRepository { + + private final JdbcTemplate jdbcTemplate; + private final int BATCH_SIZE = 1000; + + public void bulkInsertReviews(List reviews) { + String sql = "INSERT INTO review (user_id, product_id, score) values (?, ?, ?)"; + + Random random = new Random(); + + jdbcTemplate.batchUpdate(sql, reviews, BATCH_SIZE, (ps, argument) -> { + ps.setInt(1, 1); + ps.setInt(2, random.nextInt(100000) + 1); + ps.setDouble(3, random.nextDouble() * 5); + }); + } + +} diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index ba56fe1..f208901 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -13,9 +13,6 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table( - indexes = @Index(name = "idx_email", columnList = "email") -) public class User extends TimeStamped { @Id diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 6e38dc5..a309d19 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -2,9 +2,6 @@ import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.repository.ProductBulkRepository; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; -import com.example.eightyage.domain.user.repository.UserBulkRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -26,7 +23,7 @@ public class ProductBulkTest { List batchList = new ArrayList<>(); - for (int i = 0; i < 1_000; i++) { + for (int i = 0; i < 50_000; i++) { Product product = Product.builder() .name(UUID.randomUUID().toString()) .build(); @@ -36,7 +33,7 @@ public class ProductBulkTest { productBulkRepository.bulkInsertProduct(batchList); batchList.clear(); -// sleep(500); + sleep(500); } } diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java new file mode 100644 index 0000000..a009999 --- /dev/null +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -0,0 +1,50 @@ +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile(value = "test") +public class ReviewBulkTest { + + @Autowired + private ReviewBulkRepository reviewBulkRepository; + + @Test + void 리뷰_데이터_오백만건_생성() { + + List batchList = new ArrayList<>(); + + for (int i = 0; i < 5_000_000; i++) { + Review review = new Review(); + batchList.add(review); + + if (batchList.size() == 5000) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + + sleep(500); + } + } + + if (!batchList.isEmpty()) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index d599028..670c029 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -36,7 +36,7 @@ public class UserBulkTest { userBulkRepository.bulkInsertUsers(batchList); batchList.clear(); -// sleep(500); + sleep(500); } } From c6830204b7f6ede28e31d0ad2f2709138e085c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 09:34:41 +0900 Subject: [PATCH 116/164] =?UTF-8?q?test(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ProductSaveRequestDto.java | 2 + .../dto/request/ProductUpdateRequestDto.java | 2 + .../product/service/ProductServiceTest.java | 40 ++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java index c8d2a04..9cb5616 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java @@ -4,9 +4,11 @@ import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class ProductSaveRequestDto { @NotBlank(message= ValidationMessage.NOT_BLANK_PRODUCT_NAME) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java index 5b0e3ab..c7bccbd 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -2,9 +2,11 @@ import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.SaleState; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class ProductUpdateRequestDto { private String productName; diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java index 5cd2b3f..2fe9c0c 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -1,6 +1,10 @@ package com.example.eightyage.domain.product.service; +import com.example.eightyage.domain.product.dto.request.ProductSaveRequestDto; +import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; +import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.entity.Category; import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.entity.SaleState; @@ -49,11 +53,43 @@ class ProductServiceTest { private Review review2; @Test - void 제품_단건_조회_성공(){ + void 제품_생성_성공(){ + // given + Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); + + given(productRepository.save(any())).willReturn(product); + + ProductSaveRequestDto requestDto = new ProductSaveRequestDto("8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴줍니다.", 20000); + + // when + ProductSaveResponseDto savedProduct = productService.saveProduct(requestDto); + + // then + assertThat(savedProduct.getProductName()).isEqualTo(product.getName()); + } + + @Test + void 제품_수정_성공(){ // given Long productId = 1L; -// Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); + Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); + + given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); + + ProductUpdateRequestDto requestDto = new ProductUpdateRequestDto("8자 주름 향수", Category.FRAGRANCE, "8자 주름의 은은한 향기", SaleState.FOR_SALE, 50000); + + // when + ProductUpdateResponseDto responseDto = productService.updateProduct(productId, requestDto); + + // then + assertThat(responseDto.getProductName()).isEqualTo(requestDto.getProductName()); + } + + @Test + void 제품_단건_조회_성공(){ + // given + Long productId = 1L; given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); From e698d600b030e799d1346ce009af2d56d776a0d1 Mon Sep 17 00:00:00 2001 From: peridot Date: Fri, 28 Mar 2025 10:19:21 +0900 Subject: [PATCH 117/164] =?UTF-8?q?feat(coupon)=20lock=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +- .../domain/coupon/service/CouponService.java | 52 ++++++++++++++----- .../global/config/RedissonConfig.java | 19 +++++++ .../global/exception/ErrorMessage.java | 1 + 4 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/eightyage/global/config/RedissonConfig.java diff --git a/build.gradle b/build.gradle index 73d3541..8f5ea31 100644 --- a/build.gradle +++ b/build.gradle @@ -47,8 +47,9 @@ dependencies { // spring cloud AWS S3 implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0' - // redis + // redis & redisson implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson:3.23.5' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 1cfd7da..fe3219d 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -13,12 +13,16 @@ import com.example.eightyage.global.exception.ForbiddenException; import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.util.concurrent.TimeUnit; + @Service @RequiredArgsConstructor public class CouponService { @@ -28,26 +32,48 @@ public class CouponService { private final StringRedisTemplate stringRedisTemplate; private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; + private static final String EVENT_LOCK_PREFIX = "event:lock:"; + private final RedissonClient redissonClient; public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { - // 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); - if (remain == null || remain < 0) { // atomic? `DESC`? - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - Event event = eventService.getValidEventOrThrow(eventId); + RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); + boolean isLocked = false; - if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } + try { + isLocked = rLock.tryLock(3, 10, TimeUnit.SECONDS); // 3초 안에 락을 획득, 10초 뒤에는 자동 해제 - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); + if (!isLocked) { + throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 + } - couponRepository.save(coupon); + // 락 획득 -> 임계 구역 진입 + // 쿠폰 수량 우선 차감 + Long remain = stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + if (remain == 0 || remain < 0) { + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); + } - return coupon.toDto(); + Event event = eventService.getValidEventOrThrow(eventId); + + if (couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); + } + + // 쿠폰 발급 및 저장 + Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); + couponRepository.save(coupon); + + return coupon.toDto(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BadRequestException(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage()); + } finally { + if (isLocked) { + rLock.unlock(); + } + } } public Page getMyCoupons(AuthUser authUser, int page, int size) { diff --git a/src/main/java/com/example/eightyage/global/config/RedissonConfig.java b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java new file mode 100644 index 0000000..4586fb3 --- /dev/null +++ b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java @@ -0,0 +1,19 @@ +package com.example.eightyage.global.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + + @Bean + public RedissonClient redisson() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + return Redisson.create(); + } +} diff --git a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java index f319ae2..ee23e98 100644 --- a/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java +++ b/src/main/java/com/example/eightyage/global/exception/ErrorMessage.java @@ -28,6 +28,7 @@ public enum ErrorMessage { EVENT_NOT_FOUND("이벤트를 찾을 수 없습니다."), INVALID_EVENT_PERIOD("이벤트 기간이 아닙니다."), + CAN_NOT_ACCESS("잠시후 다시 시도해주세요"), COUPON_ALREADY_ISSUED("이미 쿠폰 발급 받은 사용자입니다."), COUPON_OUT_OF_STOCK("쿠폰 수량이 소진되었습니다."), COUPON_NOT_FOUND("쿠폰을 찾을 수 없습니다."), From 0e010b5789be7e5d09c534352d2692f93123c084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 11:48:49 +0900 Subject: [PATCH 118/164] =?UTF-8?q?test(review):=20=EB=A6=AC=EB=B7=B0=20CR?= =?UTF-8?q?UD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ReviewSaveRequestDto.java | 2 + .../dto/request/ReviewUpdateRequestDto.java | 2 + .../review/service/ReviewServiceTest.java | 188 ++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java index 9c1d183..5293830 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewSaveRequestDto.java @@ -3,9 +3,11 @@ import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class ReviewSaveRequestDto { @NotNull(message = ValidationMessage.NOT_NULL_SCORE) diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java index c6b3e36..6fa8ef6 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -2,8 +2,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class ReviewUpdateRequestDto { private Double score; diff --git a/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java new file mode 100644 index 0000000..0b4a973 --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java @@ -0,0 +1,188 @@ +package com.example.eightyage.domain.review.service; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.repository.ProductRepository; +import com.example.eightyage.domain.product.service.ProductService; +import com.example.eightyage.domain.review.dto.request.ReviewSaveRequestDto; +import com.example.eightyage.domain.review.dto.request.ReviewUpdateRequestDto; +import com.example.eightyage.domain.review.dto.response.ReviewSaveResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewUpdateResponseDto; +import com.example.eightyage.domain.review.dto.response.ReviewsGetResponseDto; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewRepository; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserRepository; +import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.global.exception.UnauthorizedException; +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.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.data.domain.*; +import org.springframework.data.querydsl.QPageRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ReviewServiceTest { + + @Mock + ReviewRepository reviewRepository; + + @Mock + UserService userService; + + @Mock + ProductService productService; + + @InjectMocks + ReviewService reviewService; + + @Mock + User user; + + @Mock + Product product; + + @Mock + Review review; + + @Test + void 리뷰_생성_성공(){ + // given + Long userId = 1L; + Long productId = 1L; + Long reviewId = 1L; + + Review review = new Review(reviewId, user, product, 5.0, "8자 주름을 다리미처럼 펴줘요 짱짱"); + given(reviewRepository.save(any())).willReturn(review); + + ReviewSaveRequestDto requestDto = new ReviewSaveRequestDto(5.0, "8자 주름을 다리미처럼 펴줘요 짱짱"); + + // when + ReviewSaveResponseDto responseDto = reviewService.saveReview(userId, productId, requestDto); + + // then + assertEquals(requestDto.getContent(), responseDto.getContent()); + } + + @Test + void 리뷰_수정_작성한_본인이_아닐_경우_실패(){ + // given + Long userId = 2L; + Long reviewId = 1L; + ReviewUpdateRequestDto requestDto = new ReviewUpdateRequestDto(1.0, "쓰다보니 8자 주름이 깊어졌어요. 대실망"); + + User user1 = new User(1L, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + User user2 = new User(userId, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + Review review = new Review(reviewId, user1, product, 5.0, "8자 주름을 펴줘요"); + + given(userService.findUserByIdOrElseThrow(any())).willReturn(user2); + given(reviewRepository.findById(any())).willReturn(Optional.of(review)); + + // when + UnauthorizedException exception = assertThrows(UnauthorizedException.class, () -> { + reviewService.updateReview(userId, reviewId, requestDto); + }); + + // then + assertEquals("리뷰를 수정할 권한이 없습니다.", exception.getMessage()); + } + + @Test + void 리뷰_수정_성공(){ + // given + Long userId = 1L; + Long reviewId = 1L; + ReviewUpdateRequestDto requestDto = new ReviewUpdateRequestDto(1.0, "쓰다보니 8자 주름이 깊어졌어요. 대실망"); + + User user = new User(userId, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + Review review = new Review(reviewId, user, product, 5.0, "8자 주름을 펴줘요"); + + given(userService.findUserByIdOrElseThrow(any())).willReturn(user); + given(reviewRepository.findById(any())).willReturn(Optional.of(review)); + + // when + ReviewUpdateResponseDto responseDto = reviewService.updateReview(userId, reviewId, requestDto); + + // then + assertEquals(requestDto.getContent(), responseDto.getContent()); + } + + @Test + void 리뷰_다건_조회_성공(){ + // given + Long productId = 1L; + PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "score")); + + Review review1 = new Review(1L, user, product, 5.0, "8자 주름을 펴줘요"); + Review review2 = new Review(1L, user, product, 5.0, "8자 주름을 펴줘요"); + + List reviewList = new ArrayList<>(); + reviewList.add(review1); + reviewList.add(review2); + + Page reviewPage = new PageImpl<>(reviewList, pageRequest, reviewList.size()); + + when(reviewRepository.findByProductIdAndProductDeletedAtIsNull(any(Long.class), eq(pageRequest))).thenReturn(reviewPage); + + // when + Page result = reviewService.findReviews(productId, pageRequest); + + // then + assertNotNull(result); + assertEquals(2, result.getContent().size()); + verify(reviewRepository, times(1)).findByProductIdAndProductDeletedAtIsNull(any(Long.class), eq(pageRequest)); + } + + @Test + void 리뷰_삭제_작성한_본인이_아닐_경우_실패(){ + // given + Long userId = 2L; + Long reviewId = 1L; + + User user1 = new User(1L, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + User user2 = new User(userId, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + Review review = new Review(reviewId, user1, product, 5.0, "8자 주름을 펴줘요"); + + given(userService.findUserByIdOrElseThrow(any())).willReturn(user2); + given(reviewRepository.findById(any())).willReturn(Optional.of(review)); + + // when + UnauthorizedException exception = assertThrows(UnauthorizedException.class, () -> { + reviewService.deleteReview(userId, reviewId); + }); + + // then + assertEquals("리뷰를 삭제할 권한이 없습니다.", exception.getMessage()); + } + + @Test + void 리뷰_삭제_성공(){ + // given + Long userId = 1L; + Long reviewId = 1L; + + User user = new User(userId, "ijieun@gmail.com", "이지은B", "password123", UserRole.ROLE_USER); + + given(userService.findUserByIdOrElseThrow(any())).willReturn(user); + given(reviewRepository.findById(any())).willReturn(Optional.of(review)); + given(review.getUser()).willReturn(user); + + // when + reviewService.deleteReview(userId, reviewId); + + // then + verify(review, times(1)).delete(); + } +} \ No newline at end of file From 37e3d92191757564a747cb82e491fbc9fb40cd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 13:45:43 +0900 Subject: [PATCH 119/164] =?UTF-8?q?test(productImage):=20mock=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ProductImageServiceTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java new file mode 100644 index 0000000..e05f6f0 --- /dev/null +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java @@ -0,0 +1,46 @@ +package com.example.eightyage.domain.product.service; + +import com.example.eightyage.domain.product.repository.ProductImageRepository; +import org.junit.jupiter.api.BeforeEach; +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.mock.web.MockMultipartFile; +import software.amazon.awssdk.services.s3.S3Client; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class ProductImageServiceTest { + + @Mock + S3Client s3Client; + + @Mock + ProductImageRepository productImageRepository; + + @Mock + ProductService productService; + + @InjectMocks + ProductImageService productImageService; + + private MockMultipartFile mockFile; + + @BeforeEach + void setUp(){ + mockFile = new MockMultipartFile( + "file", + "tesst.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + @Test + void 이미지_업로드_성공(){ + + } +} \ No newline at end of file From 8ca619dc595b34c7dcf616bfaf801b193db7e6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 13:54:02 +0900 Subject: [PATCH 120/164] =?UTF-8?q?config:=20Redis=20=ED=98=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20S3=20=EB=B2=84=ED=82=B7=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20env=20=ED=8C=8C=EC=9D=BC=EC=97=90=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 --- src/main/resources/application.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 44a0d39..164064b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,7 +40,7 @@ spring: region: static: ap-northeast-2 s3: - bucket: my-gom-bucket + bucket: ${S3_BUCKET} aws: credentials: @@ -48,11 +48,11 @@ aws: secret-key: ${S3_SECRET_KEY} region: ap-northeast-2 s3: - bucket: my-gom-bucket + bucket: ${S3_BUCKET} data: redis: - host: localhost + host: ${REDIS_HOST} port: 6379 jwt: From 1281c85d47bc749ea47b584ac1c5709f49a381f5 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 15:24:30 +0900 Subject: [PATCH 121/164] =?UTF-8?q?fix(search):=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- .../domain/review/repository/ReviewBulkRepository.java | 2 +- .../java/com/example/eightyage/bulk/ProductBulkTest.java | 8 ++++---- .../java/com/example/eightyage/bulk/ReviewBulkTest.java | 8 ++++---- .../java/com/example/eightyage/bulk/UserBulkTest.java | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java index 584e4bd..7b72dd5 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java @@ -22,7 +22,7 @@ public void bulkInsertReviews(List reviews) { jdbcTemplate.batchUpdate(sql, reviews, BATCH_SIZE, (ps, argument) -> { ps.setInt(1, 1); - ps.setInt(2, random.nextInt(100000) + 1); + ps.setInt(2, random.nextInt(1000) + 1); ps.setDouble(3, random.nextDouble() * 5); }); } diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index a309d19..eead7ab 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -12,24 +12,24 @@ import java.util.UUID; @SpringBootTest -@Profile(value = "test") +@Profile(value = "ci") public class ProductBulkTest { @Autowired private ProductBulkRepository productBulkRepository; @Test - void 제품_데이터_백만건_생성() { + void 제품_더미데이터_생성() { List batchList = new ArrayList<>(); - for (int i = 0; i < 50_000; i++) { + for (int i = 0; i < 1000; i++) { Product product = Product.builder() .name(UUID.randomUUID().toString()) .build(); batchList.add(product); - if (batchList.size() == 50) { + if (batchList.size() == 1000) { productBulkRepository.bulkInsertProduct(batchList); batchList.clear(); diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index a009999..6903bee 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -11,22 +11,22 @@ import java.util.List; @SpringBootTest -@Profile(value = "test") +@Profile(value = "ci") public class ReviewBulkTest { @Autowired private ReviewBulkRepository reviewBulkRepository; @Test - void 리뷰_데이터_오백만건_생성() { + void 리뷰_더미데이터_생성() { List batchList = new ArrayList<>(); - for (int i = 0; i < 5_000_000; i++) { + for (int i = 0; i < 1000; i++) { Review review = new Review(); batchList.add(review); - if (batchList.size() == 5000) { + if (batchList.size() == 1000) { reviewBulkRepository.bulkInsertReviews(batchList); batchList.clear(); diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 670c029..78b8962 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -12,7 +12,7 @@ import java.util.List; @SpringBootTest -@Profile(value = "test") +@Profile(value = "ci") public class UserBulkTest { @Autowired @@ -23,7 +23,7 @@ public class UserBulkTest { List batchList = new ArrayList<>(); - for (int i = 0; i < 1_000_000; i++) { + for (int i = 0; i < 1000; i++) { User user = User.builder() .email(i + "@email.com") .nickname("nickname" + i) From a775d9db4ba117347d3ded7b89ecf3d33ffd9597 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 15:35:23 +0900 Subject: [PATCH 122/164] =?UTF-8?q?test(search):=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=EC=8B=A4=ED=8E=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- .../eightyage/bulk/ProductBulkTest.java | 106 ++++++++--------- .../eightyage/bulk/ReviewBulkTest.java | 100 ++++++++-------- .../example/eightyage/bulk/UserBulkTest.java | 112 +++++++++--------- 3 files changed, 159 insertions(+), 159 deletions(-) diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index eead7ab..19bb3fb 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -1,53 +1,53 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.repository.ProductBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@SpringBootTest -@Profile(value = "ci") -public class ProductBulkTest { - - @Autowired - private ProductBulkRepository productBulkRepository; - - @Test - void 제품_더미데이터_생성() { - - List batchList = new ArrayList<>(); - - for (int i = 0; i < 1000; i++) { - Product product = Product.builder() - .name(UUID.randomUUID().toString()) - .build(); - batchList.add(product); - - if (batchList.size() == 1000) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.product.entity.Product; +//import com.example.eightyage.domain.product.repository.ProductBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.context.annotation.Profile; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.UUID; +// +//@SpringBootTest +//@Profile(value = "ci") +//public class ProductBulkTest { +// +// @Autowired +// private ProductBulkRepository productBulkRepository; +// +// @Test +// void 제품_더미데이터_생성() { +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < 1000; i++) { +// Product product = Product.builder() +// .name(UUID.randomUUID().toString()) +// .build(); +// batchList.add(product); +// +// if (batchList.size() == 1000) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 6903bee..0d845f6 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -1,50 +1,50 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.review.entity.Review; -import com.example.eightyage.domain.review.repository.ReviewBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; - -import java.util.ArrayList; -import java.util.List; - -@SpringBootTest -@Profile(value = "ci") -public class ReviewBulkTest { - - @Autowired - private ReviewBulkRepository reviewBulkRepository; - - @Test - void 리뷰_더미데이터_생성() { - - List batchList = new ArrayList<>(); - - for (int i = 0; i < 1000; i++) { - Review review = new Review(); - batchList.add(review); - - if (batchList.size() == 1000) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.review.entity.Review; +//import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.context.annotation.Profile; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@SpringBootTest +//@Profile(value = "ci") +//public class ReviewBulkTest { +// +// @Autowired +// private ReviewBulkRepository reviewBulkRepository; +// +// @Test +// void 리뷰_더미데이터_생성() { +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < 1000; i++) { +// Review review = new Review(); +// batchList.add(review); +// +// if (batchList.size() == 1000) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 78b8962..bcc530f 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,56 +1,56 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; -import com.example.eightyage.domain.user.repository.UserBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; - -import java.util.ArrayList; -import java.util.List; - -@SpringBootTest -@Profile(value = "ci") -public class UserBulkTest { - - @Autowired - private UserBulkRepository userBulkRepository; - - @Test - void 유저_데이터_백만건_생성() { - - List batchList = new ArrayList<>(); - - for (int i = 0; i < 1000; i++) { - User user = User.builder() - .email(i + "@email.com") - .nickname("nickname" + i) - .password("password") - .userRole(UserRole.ROLE_USER) - .build(); - batchList.add(user); - - if (batchList.size() == 1000) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.domain.user.entity.UserRole; +//import com.example.eightyage.domain.user.repository.UserBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.context.annotation.Profile; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@SpringBootTest +//@Profile(value = "ci") +//public class UserBulkTest { +// +// @Autowired +// private UserBulkRepository userBulkRepository; +// +// @Test +// void 유저_데이터_백만건_생성() { +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < 1000; i++) { +// User user = User.builder() +// .email(i + "@email.com") +// .nickname("nickname" + i) +// .password("password") +// .userRole(UserRole.ROLE_USER) +// .build(); +// batchList.add(user); +// +// if (batchList.size() == 1000) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} From 9ca8c6e8e9a8753c22bc7b48088929cc93637995 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 15:44:15 +0900 Subject: [PATCH 123/164] =?UTF-8?q?test(search):=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=8D=94=EB=AF=B8=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- .../eightyage/bulk/ProductBulkTest.java | 114 +++++++++-------- .../eightyage/bulk/ReviewBulkTest.java | 108 ++++++++-------- .../example/eightyage/bulk/UserBulkTest.java | 120 ++++++++++-------- 3 files changed, 183 insertions(+), 159 deletions(-) diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 19bb3fb..c5402d8 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -1,53 +1,61 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.product.entity.Product; -//import com.example.eightyage.domain.product.repository.ProductBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.context.annotation.Profile; -// -//import java.util.ArrayList; -//import java.util.List; -//import java.util.UUID; -// -//@SpringBootTest -//@Profile(value = "ci") -//public class ProductBulkTest { -// -// @Autowired -// private ProductBulkRepository productBulkRepository; -// -// @Test -// void 제품_더미데이터_생성() { -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < 1000; i++) { -// Product product = Product.builder() -// .name(UUID.randomUUID().toString()) -// .build(); -// batchList.add(product); -// -// if (batchList.size() == 1000) { -// productBulkRepository.bulkInsertProduct(batchList); -// batchList.clear(); -// -// sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// productBulkRepository.bulkInsertProduct(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.repository.ProductBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@SpringBootTest +@Profile(value = "ci") +public class ProductBulkTest { + + @Autowired + private ProductBulkRepository productBulkRepository; + + @Test + void 제품_더미데이터_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + Product product = Product.builder() + .name(UUID.randomUUID().toString()) + .build(); + batchList.add(product); + + if (batchList.size() == insertCount) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + + sleep(500); + } + } + + if (!batchList.isEmpty()) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 0d845f6..07af9d3 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -1,50 +1,58 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.review.entity.Review; -//import com.example.eightyage.domain.review.repository.ReviewBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.context.annotation.Profile; -// -//import java.util.ArrayList; -//import java.util.List; -// -//@SpringBootTest -//@Profile(value = "ci") -//public class ReviewBulkTest { -// -// @Autowired -// private ReviewBulkRepository reviewBulkRepository; -// -// @Test -// void 리뷰_더미데이터_생성() { -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < 1000; i++) { -// Review review = new Review(); -// batchList.add(review); -// -// if (batchList.size() == 1000) { -// reviewBulkRepository.bulkInsertReviews(batchList); -// batchList.clear(); -// -// sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// reviewBulkRepository.bulkInsertReviews(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile(value = "ci") +public class ReviewBulkTest { + + @Autowired + private ReviewBulkRepository reviewBulkRepository; + + @Test + void 리뷰_더미데이터_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + Review review = new Review(); + batchList.add(review); + + if (batchList.size() == insertCount) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + + sleep(500); + } + } + + if (!batchList.isEmpty()) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index bcc530f..362645b 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,56 +1,64 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.user.entity.User; -//import com.example.eightyage.domain.user.entity.UserRole; -//import com.example.eightyage.domain.user.repository.UserBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.context.annotation.Profile; -// -//import java.util.ArrayList; -//import java.util.List; -// -//@SpringBootTest -//@Profile(value = "ci") -//public class UserBulkTest { -// -// @Autowired -// private UserBulkRepository userBulkRepository; -// -// @Test -// void 유저_데이터_백만건_생성() { -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < 1000; i++) { -// User user = User.builder() -// .email(i + "@email.com") -// .nickname("nickname" + i) -// .password("password") -// .userRole(UserRole.ROLE_USER) -// .build(); -// batchList.add(user); -// -// if (batchList.size() == 1000) { -// userBulkRepository.bulkInsertUsers(batchList); -// batchList.clear(); -// -// sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// userBulkRepository.bulkInsertUsers(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.repository.UserBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@Profile(value = "ci") +public class UserBulkTest { + + @Autowired + private UserBulkRepository userBulkRepository; + + @Test + void 유저_데이터_백만건_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + User user = User.builder() + .email(i + "@email.com") + .nickname("nickname" + i) + .password("password") + .userRole(UserRole.ROLE_USER) + .build(); + batchList.add(user); + + if (batchList.size() == insertCount) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + + sleep(500); + } + } + + if (!batchList.isEmpty()) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} From b5b571ce49e784c5d1bddabfec16acb0835401a7 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 15:54:49 +0900 Subject: [PATCH 124/164] =?UTF-8?q?test(search):=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20@ActiveProfiles("ci")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- src/test/java/com/example/eightyage/bulk/ProductBulkTest.java | 4 ++-- src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java | 4 ++-- src/test/java/com/example/eightyage/bulk/UserBulkTest.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index c5402d8..8c0c98a 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -5,14 +5,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; import java.util.ArrayList; import java.util.List; import java.util.UUID; @SpringBootTest -@Profile(value = "ci") +@ActiveProfiles("ci") public class ProductBulkTest { @Autowired diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 07af9d3..44ce0c0 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -5,13 +5,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; import java.util.ArrayList; import java.util.List; @SpringBootTest -@Profile(value = "ci") +@ActiveProfiles("ci") public class ReviewBulkTest { @Autowired diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 362645b..7069652 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -6,13 +6,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; import java.util.ArrayList; import java.util.List; @SpringBootTest -@Profile(value = "ci") +@ActiveProfiles("ci") public class UserBulkTest { @Autowired From 16f521c69231963c71340429b200cc12fdb21368 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 16:11:57 +0900 Subject: [PATCH 125/164] =?UTF-8?q?test(search):=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20application-local.yml=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- .gitignore | 4 ++-- src/main/resources/application-local.yml | 18 ++++++++++++++++++ src/main/resources/application.yml | 18 ------------------ src/test/resources/application-test.yml | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/main/resources/application-local.yml create mode 100644 src/test/resources/application-test.yml diff --git a/.gitignore b/.gitignore index 057b280..2797dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,8 @@ Thumbs.db .env # Spring Boot application properties/yaml -application-*.yml -application-*.properties +#application-*.yml +#application-*.properties # AWS config .aws/ diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..610611b --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,18 @@ +spring: + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: + static: ap-northeast-2 + s3: + bucket: my-gom-bucket + +aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: ap-northeast-2 + s3: + bucket: my-gom-bucket \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f3ef6e1..f87da91 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -37,24 +37,6 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect - cloud: - aws: - credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} - region: - static: ap-northeast-2 - s3: - bucket: my-gom-bucket - -aws: - credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} - region: ap-northeast-2 - s3: - bucket: my-gom-bucket - jwt: secret: key: ${JWT_SECRET_KEY} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..da95d4b --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,16 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: true + dialect: org.hibernate.dialect.MySQLDialect \ No newline at end of file From c61e63ed7890fb0f3d80bac4212deff445036273 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 16:33:27 +0900 Subject: [PATCH 126/164] =?UTF-8?q?test(search):=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20-=20application-local.yml=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 구현한 것 - 더미데이터 생성 볼륨을 줄여 CI 빌드 실패가 되는 점을 수정 --- src/main/resources/application-ci.yml | 10 +- .../eightyage/bulk/ProductBulkTest.java | 122 ++++++++--------- .../eightyage/bulk/ReviewBulkTest.java | 116 ++++++++-------- .../example/eightyage/bulk/UserBulkTest.java | 128 +++++++++--------- 4 files changed, 192 insertions(+), 184 deletions(-) diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index fb7a102..1110e9e 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -18,4 +18,12 @@ spring: format_sql: true jwt: secret: - key: ${JWT_SECRET_KEY} \ No newline at end of file + key: ${JWT_SECRET_KEY} + +aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: ap-northeast-2 + s3: + bucket: my-gom-bucket \ No newline at end of file diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 8c0c98a..3b2584c 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -1,61 +1,61 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.repository.ProductBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@SpringBootTest -@ActiveProfiles("ci") -public class ProductBulkTest { - - @Autowired - private ProductBulkRepository productBulkRepository; - - @Test - void 제품_더미데이터_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - Product product = Product.builder() - .name(UUID.randomUUID().toString()) - .build(); - batchList.add(product); - - if (batchList.size() == insertCount) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.product.entity.Product; +//import com.example.eightyage.domain.product.repository.ProductBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.UUID; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class ProductBulkTest { +// +// @Autowired +// private ProductBulkRepository productBulkRepository; +// +// @Test +// void 제품_더미데이터_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 100; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// Product product = Product.builder() +// .name(UUID.randomUUID().toString()) +// .build(); +// batchList.add(product); +// +// if (batchList.size() == insertCount) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 44ce0c0..ebd86aa 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -1,58 +1,58 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.review.entity.Review; -import com.example.eightyage.domain.review.repository.ReviewBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("ci") -public class ReviewBulkTest { - - @Autowired - private ReviewBulkRepository reviewBulkRepository; - - @Test - void 리뷰_더미데이터_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - Review review = new Review(); - batchList.add(review); - - if (batchList.size() == insertCount) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.review.entity.Review; +//import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class ReviewBulkTest { +// +// @Autowired +// private ReviewBulkRepository reviewBulkRepository; +// +// @Test +// void 리뷰_더미데이터_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 100; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// Review review = new Review(); +// batchList.add(review); +// +// if (batchList.size() == insertCount) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 7069652..084b16f 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,64 +1,64 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; -import com.example.eightyage.domain.user.repository.UserBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("ci") -public class UserBulkTest { - - @Autowired - private UserBulkRepository userBulkRepository; - - @Test - void 유저_데이터_백만건_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - User user = User.builder() - .email(i + "@email.com") - .nickname("nickname" + i) - .password("password") - .userRole(UserRole.ROLE_USER) - .build(); - batchList.add(user); - - if (batchList.size() == insertCount) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.domain.user.entity.UserRole; +//import com.example.eightyage.domain.user.repository.UserBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class UserBulkTest { +// +// @Autowired +// private UserBulkRepository userBulkRepository; +// +// @Test +// void 유저_데이터_백만건_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 100; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// User user = User.builder() +// .email(i + "@email.com") +// .nickname("nickname" + i) +// .password("password") +// .userRole(UserRole.ROLE_USER) +// .build(); +// batchList.add(user); +// +// if (batchList.size() == insertCount) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} From 2a4bc3ac64224d442249ee2d33a00ac133d5476e Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Fri, 28 Mar 2025 16:54:40 +0900 Subject: [PATCH 127/164] =?UTF-8?q?feat(search):=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=9D=B8=EA=B8=B0=20=EA=B2=80=EC=83=89=EC=96=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/controller/ProductController.java | 11 ++++++ .../product/service/ProductService.java | 20 ++++++++++- .../search/controller/SearchController.java | 10 ++++++ .../domain/search/dto/PopularKeywordDto.java | 3 ++ .../{ => v2}/KeywordCountFlushService.java | 2 +- .../search/service/v2/SearchServiceV2.java | 1 + .../service/v3/PopularKeywordServiceV3.java | 34 ++++++++++++++++++ .../search/service/v3/SearchServiceV3.java | 36 +++++++++++++++++++ 8 files changed, 115 insertions(+), 2 deletions(-) rename src/main/java/com/example/eightyage/domain/search/service/{ => v2}/KeywordCountFlushService.java (97%) create mode 100644 src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java create mode 100644 src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index b510d58..54c28ed 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -74,6 +74,17 @@ public ResponseEntity> searchProductV2( return ResponseEntity.ok(productService.getProductsV2(name, category, size, page)); } + // 제품 다건 조회 version 3 + @GetMapping("/v3/products") + public ResponseEntity> searchProductV3( + @RequestParam(required = false) String name, + @RequestParam(required = false) Category category, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "1") int page + ) { + return ResponseEntity.ok(productService.getProductsV3(name, category, size, page)); + } + // 제품 삭제 @Secured("ROLE_ADMIN") @DeleteMapping("/v1/products/{productId}") diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index e1ecf5d..9d4e986 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -11,6 +11,7 @@ import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.domain.search.service.v1.SearchServiceV1; import com.example.eightyage.domain.search.service.v2.SearchServiceV2; +import com.example.eightyage.domain.search.service.v3.SearchServiceV3; import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -31,6 +32,7 @@ public class ProductService { private final ReviewRepository reviewRepository; private final SearchServiceV1 searchServiceV1; private final SearchServiceV2 searchServiceV2; + private final SearchServiceV3 searchServiceV3; // 제품 생성 @Transactional @@ -116,13 +118,27 @@ public Page getProductsV2(String productName, Category return productsResponse; } + // 제품 다건 조회 version 3 + @Transactional(readOnly = true) + public Page getProductsV3(String productName, Category category, int size, int page) { + int adjustedPage = Math.max(0, page - 1); + Pageable pageable = PageRequest.of(adjustedPage, size); + Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); + + if(StringUtils.hasText(productName) && !productsResponse.isEmpty()){ + searchServiceV3.saveSearchLog(productName); + searchServiceV3.increaseSortedKeywordRank(productName); + } + return productsResponse; + } + // 제품 삭제 @Transactional public void deleteProduct(Long productId) { Product findProduct = findProductByIdOrElseThrow(productId); List findReviewList = reviewRepository.findReviewsByProductId(productId); - for(Review review : findReviewList){ + for (Review review : findReviewList) { review.delete(); } @@ -134,4 +150,6 @@ public Product findProductByIdOrElseThrow(Long productId) { () -> new NotFoundException("해당 제품이 존재하지 않습니다.") ); } + + } diff --git a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java index 3bfbcc2..6ef8d33 100644 --- a/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java +++ b/src/main/java/com/example/eightyage/domain/search/controller/SearchController.java @@ -3,6 +3,7 @@ import com.example.eightyage.domain.search.dto.PopularKeywordDto; import com.example.eightyage.domain.search.service.v1.PopularKeywordServiceV1; import com.example.eightyage.domain.search.service.v2.PopularKeywordServiceV2; +import com.example.eightyage.domain.search.service.v3.PopularKeywordServiceV3; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,6 +18,7 @@ public class SearchController { private final PopularKeywordServiceV1 popularKeywordServiceV1; private final PopularKeywordServiceV2 popularKeywordServiceV2; + private final PopularKeywordServiceV3 popularKeywordServiceV3; // 인기 검색어 조회 (캐시 X) @GetMapping("/api/v1/search/popular") @@ -33,4 +35,12 @@ public ResponseEntity> searchPopularKeywordsV2( ) { return ResponseEntity.ok(popularKeywordServiceV2.searchPopularKeywords(days)); } + + // 실시간 인기 검색어 조회 (캐시 O) + @GetMapping("/api/v3/search/popular") + public ResponseEntity> searchPopularKeywordsV3( + @RequestParam(defaultValue = "10") int limits + ) { + return ResponseEntity.ok(popularKeywordServiceV3.searchPopularKeywords(limits)); + } } diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java index 5f7a39f..89eac11 100644 --- a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -14,4 +14,7 @@ public class PopularKeywordDto { private String keyword; private Long count; + public static PopularKeywordDto of(String keyword, Long score) { + return new PopularKeywordDto(keyword, score); + } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java similarity index 97% rename from src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java rename to src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java index f6d3832..b7aa890 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/KeywordCountFlushService.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.search.service; +package com.example.eightyage.domain.search.service.v2; import com.example.eightyage.domain.search.entity.KeywordCount; import com.example.eightyage.domain.search.repository.KeywordCountRepository; diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index 8b9b721..c2928a6 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -47,6 +47,7 @@ public void increaseKeywordCount(String keyword) { updateKeywordSet(keyword); } + // 캐시에 키워드 추가 private void updateKeywordSet(String keyword) { Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); if (keySetCache != null) { diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java new file mode 100644 index 0000000..843a561 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java @@ -0,0 +1,34 @@ +package com.example.eightyage.domain.search.service.v3; + +import com.example.eightyage.domain.search.dto.PopularKeywordDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PopularKeywordServiceV3 { + + private final RedisTemplate redisTemplate; + private static final String RANKING_KEY = "rankingPopularKeywords"; + + // 인기 검색어 상위 N개 조회 + @Transactional(readOnly = true) + public List searchPopularKeywords(int limit) { + Set> keywordSet = + redisTemplate.opsForZSet().reverseRangeWithScores(RANKING_KEY, 0, limit - 1); + + if (keywordSet == null) { + return List.of(); + } + return keywordSet.stream().map(tuple -> PopularKeywordDto.of(tuple.getValue(), Objects.requireNonNull(tuple.getScore()).longValue())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java new file mode 100644 index 0000000..13e04aa --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.search.service.v3; + +import com.example.eightyage.domain.search.entity.SearchLog; +import com.example.eightyage.domain.search.repository.SearchLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.Duration; + + +@Service +@RequiredArgsConstructor +public class SearchServiceV3 { + + private final SearchLogRepository searchLogRepository; + private final RedisTemplate redisTemplate; + private static final String RANKING_KEY = "rankingPopularKeywords"; + + // 검색 키워드를 로그에 저장 + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveSearchLog(String keyword) { + if (StringUtils.hasText(keyword)) { + searchLogRepository.save(SearchLog.of(keyword)); + } + } + + // 검색어 점수 증가 + public void increaseSortedKeywordRank(String productName) { + redisTemplate.opsForZSet().incrementScore(RANKING_KEY, productName, 1); + redisTemplate.expire(RANKING_KEY, Duration.ofMinutes(1)); + } +} From cc78153f65fee1147dd0dda87934df399d2d3cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 17:01:05 +0900 Subject: [PATCH 128/164] =?UTF-8?q?test(productImage):=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ProductImageRepository.java | 1 - .../service/ProductImageServiceTest.java | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java index a29cc6c..ed16385 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java @@ -1,7 +1,6 @@ package com.example.eightyage.domain.product.repository; import com.example.eightyage.domain.product.entity.ProductImage; -import com.example.eightyage.global.exception.NotFoundException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java index e05f6f0..77db7ee 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java @@ -1,16 +1,29 @@ package com.example.eightyage.domain.product.service; +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.ProductImage; import com.example.eightyage.domain.product.repository.ProductImageRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import static org.junit.jupiter.api.Assertions.*; +import java.util.Optional; +import java.util.function.Consumer; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class ProductImageServiceTest { @@ -27,6 +40,9 @@ class ProductImageServiceTest { @InjectMocks ProductImageService productImageService; + @Mock + ProductImage productImage; + private MockMultipartFile mockFile; @BeforeEach @@ -37,10 +53,40 @@ void setUp(){ "image/jpeg", "test image content".getBytes() ); + + ReflectionTestUtils.setField(productImageService, "bucket", "test-bucket"); + ReflectionTestUtils.setField(productImageService, "region", "us-west-2"); } @Test void 이미지_업로드_성공(){ + // given + Long productId = 1L; + String bucket = "test-bucket"; + String region = "us-west-2"; + String expectedImageUrl = String.format("https://%s.s3.%s.amazonaws.com/", bucket, region); + + given(productImageRepository.save(any())).willReturn(productImage); + + // when + String imageUrl = productImageService.uploadImage(productId, mockFile); + + // then + assertTrue(imageUrl.startsWith(expectedImageUrl)); + } + + @Test + void 이미지_삭제_성공(){ + // given + Long imageId = 1L; + String imageUrl = "imageUrl-example"; + + given(productImageRepository.findById(any())).willReturn(Optional.of(productImage)); + + // when + productImageService.deleteImage(imageId); + // then + verify(productImage, times(1)).delete(); } } \ No newline at end of file From 081e6a832101670d71112a20fec10a39d43e0970 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Fri, 28 Mar 2025 17:04:04 +0900 Subject: [PATCH 129/164] =?UTF-8?q?fix(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/repository/ProductRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index f018fd1..6158523 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -19,7 +19,7 @@ public interface ProductRepository extends JpaRepository { Optional findById(@Param("productId") Long productId); @Query("SELECT new com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto(p.name, p.category, p.price, AVG(r.score)) " + - "FROM Product p JOIN p.reviews r " + + "FROM Product p LEFT JOIN p.reviews r " + "WHERE p.saleState = 'FOR_SALE' " + "AND (:category IS NULL OR p.category = :category) " + "AND (:name IS NULL OR p.name LIKE CONCAT('%', :name, '%')) " + From e4caf73b11bb5c8dd5462e4ba7b1c4534939710e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 17:13:18 +0900 Subject: [PATCH 130/164] =?UTF-8?q?test(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=9C=ED=92=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/service/ProductServiceTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java index 2fe9c0c..811dc93 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -55,8 +55,6 @@ class ProductServiceTest { @Test void 제품_생성_성공(){ // given - Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); - given(productRepository.save(any())).willReturn(product); ProductSaveRequestDto requestDto = new ProductSaveRequestDto("8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴줍니다.", 20000); @@ -72,8 +70,11 @@ class ProductServiceTest { void 제품_수정_성공(){ // given Long productId = 1L; + List reviewList = new ArrayList<>(); + reviewList.add(review1); + reviewList.add(review2); - Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE); + Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE, reviewList); given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); From 000e65b1c20e1369b306541315ea71c38595c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 17:15:01 +0900 Subject: [PATCH 131/164] =?UTF-8?q?refactor:=20Redis=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 76 +++---- .../domain/coupon/service/CouponService.java | 152 ++++++------- .../event/controller/EventController.java | 74 +++--- .../domain/event/service/EventService.java | 210 +++++++++--------- 4 files changed, 256 insertions(+), 256 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index c1e1939..a57223a 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,38 +1,38 @@ -//package com.example.eightyage.domain.coupon.controller; -// -//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -//import com.example.eightyage.domain.coupon.service.CouponService; -//import com.example.eightyage.global.dto.AuthUser; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.http.ResponseEntity; -//import org.springframework.security.core.annotation.AuthenticationPrincipal; -//import org.springframework.web.bind.annotation.*; -// -//import java.util.List; -// -//@RestController -//@RequestMapping("/api") -//@RequiredArgsConstructor -//public class CouponController { -// -// private final CouponService couponService; -// -// @PostMapping("/v1/events/{eventId}/coupons") -// public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { -// return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); -// } -// -// @GetMapping("/v1/coupons/my") -// public ResponseEntity> getMyCoupons( -// @AuthenticationPrincipal AuthUser authUser, -// @RequestParam(defaultValue = "1") int page, -// @RequestParam(defaultValue = "10") int size) { -// return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); -// } -// -// @GetMapping("/v1/coupons/{couponId}") -// public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { -// return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); -// } -//} +package com.example.eightyage.domain.coupon.controller; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.service.CouponService; +import com.example.eightyage.global.dto.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class CouponController { + + private final CouponService couponService; + + @PostMapping("/v1/events/{eventId}/coupons") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { + return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); + } + + @GetMapping("/v1/coupons/my") + public ResponseEntity> getMyCoupons( + @AuthenticationPrincipal AuthUser authUser, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); + } + + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { + return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 2852a82..81a2d66 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,76 +1,76 @@ -//package com.example.eightyage.domain.coupon.service; -// -//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -//import com.example.eightyage.domain.coupon.entity.Coupon; -//import com.example.eightyage.domain.coupon.entity.CouponState; -//import com.example.eightyage.domain.coupon.repository.CouponRepository; -//import com.example.eightyage.domain.event.entity.Event; -//import com.example.eightyage.domain.event.service.EventService; -//import com.example.eightyage.domain.user.entity.User; -//import com.example.eightyage.global.dto.AuthUser; -//import com.example.eightyage.global.exception.BadRequestException; -//import com.example.eightyage.global.exception.ErrorMessage; -//import com.example.eightyage.global.exception.ForbiddenException; -//import com.example.eightyage.global.exception.NotFoundException; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageRequest; -//import org.springframework.data.domain.Pageable; -//import org.springframework.data.redis.core.StringRedisTemplate; -//import org.springframework.stereotype.Service; -// -//@Service -//@RequiredArgsConstructor -//public class CouponService { -// -// private final CouponRepository couponRepository; -// private final EventService eventService; -// private final StringRedisTemplate stringRedisTemplate; -// -// public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { -// // 수량 우선 차감 -// Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); -// if (remain == null || remain < 0) { // atomic? `DESC`? -// throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); -// } -// -// Event event = eventService.getValidEventOrThrow(eventId); -// -// if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { -// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); -// } -// -// // 쿠폰 발급 및 저장 -// Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); -// -// couponRepository.save(coupon); -// -// return coupon.toDto(); -// } -// -// public Page getMyCoupons(AuthUser authUser, int page, int size) { -// Pageable pageable = PageRequest.of(page-1, size); -// Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); -// -// return coupons.map(Coupon::toDto); -// } -// -// public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { -// Coupon coupon = findByIdOrElseThrow(couponId); -// -// if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { -// throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); -// } -// -// if(!coupon.getState().equals(CouponState.VALID)) { -// throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); -// } -// -// return coupon.toDto(); -// } -// -// public Coupon findByIdOrElseThrow(Long couponId) { -// return couponRepository.findById(couponId) -// .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); -// } -//} +package com.example.eightyage.domain.coupon.service; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.repository.CouponRepository; +import com.example.eightyage.domain.event.entity.Event; +import com.example.eightyage.domain.event.service.EventService; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; +import com.example.eightyage.global.exception.ForbiddenException; +import com.example.eightyage.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CouponService { + + private final CouponRepository couponRepository; + private final EventService eventService; + private final StringRedisTemplate stringRedisTemplate; + + public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + // 수량 우선 차감 + Long remain = stringRedisTemplate.opsForValue().decrement("event:quantity:" + eventId); + if (remain == null || remain < 0) { // atomic? `DESC`? + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); + } + + Event event = eventService.getValidEventOrThrow(eventId); + + if(couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); + } + + // 쿠폰 발급 및 저장 + Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); + + couponRepository.save(coupon); + + return coupon.toDto(); + } + + public Page getMyCoupons(AuthUser authUser, int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + + return coupons.map(Coupon::toDto); + } + + public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { + Coupon coupon = findByIdOrElseThrow(couponId); + + if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { + throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); + } + + if(!coupon.getState().equals(CouponState.VALID)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); + } + + return coupon.toDto(); + } + + public Coupon findByIdOrElseThrow(Long couponId) { + return couponRepository.findById(couponId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 3d3104c..7c12b97 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -1,37 +1,37 @@ -//package com.example.eightyage.domain.event.controller; -// -//import com.example.eightyage.domain.event.dto.request.EventRequestDto; -//import com.example.eightyage.domain.event.dto.response.EventResponseDto; -//import com.example.eightyage.domain.event.service.EventService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.*; -// -//@RestController -//@RequestMapping("/api") -//@RequiredArgsConstructor -//public class EventController { -// -// private final EventService eventService; -// -// @PostMapping("/v1/events") -// public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { -// return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); -// } -// -// @GetMapping("/v1/events") -// public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { -// return ResponseEntity.ok(eventService.getEvents(page, size)); -// } -// -// @GetMapping("/v1/events/{eventId}") -// public ResponseEntity getEvent(@PathVariable long eventId) { -// return ResponseEntity.ok(eventService.getEvent(eventId)); -// } -// -// @PatchMapping("/v1/events/{eventId}") -// public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { -// return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); -// } -//} +package com.example.eightyage.domain.event.controller; + +import com.example.eightyage.domain.event.dto.request.EventRequestDto; +import com.example.eightyage.domain.event.dto.response.EventResponseDto; +import com.example.eightyage.domain.event.service.EventService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class EventController { + + private final EventService eventService; + + @PostMapping("/v1/events") + public ResponseEntity createEvent(@RequestBody EventRequestDto eventRequestDto) { + return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); + } + + @GetMapping("/v1/events") + public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(eventService.getEvents(page, size)); + } + + @GetMapping("/v1/events/{eventId}") + public ResponseEntity getEvent(@PathVariable long eventId) { + return ResponseEntity.ok(eventService.getEvent(eventId)); + } + + @PatchMapping("/v1/events/{eventId}") + public ResponseEntity updateEvent(@PathVariable long eventId, @RequestBody EventRequestDto eventRequestDto) { + return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index 539a7d8..a96f6e1 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -1,105 +1,105 @@ -//package com.example.eightyage.domain.event.service; -// -//import com.example.eightyage.domain.event.dto.request.EventRequestDto; -//import com.example.eightyage.domain.event.dto.response.EventResponseDto; -//import com.example.eightyage.domain.event.entity.Event; -//import com.example.eightyage.domain.event.entity.EventState; -//import com.example.eightyage.domain.event.repository.EventRepository; -//import com.example.eightyage.global.exception.BadRequestException; -//import com.example.eightyage.global.exception.ErrorMessage; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.data.domain.PageRequest; -//import org.springframework.data.domain.Pageable; -//import org.springframework.data.redis.core.StringRedisTemplate; -//import org.springframework.security.access.annotation.Secured; -//import org.springframework.stereotype.Service; -// -//import java.time.LocalDateTime; -// -//@Service -//@RequiredArgsConstructor -//public class EventService { -// -// private final EventRepository eventRepository; -// private final StringRedisTemplate stringRedisTemplate; -// -// @Secured("ADMIN") -// public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { -// Event event = new Event( -// eventRequestDto.getName(), -// eventRequestDto.getDescription(), -// eventRequestDto.getQuantity(), -// eventRequestDto.getStartDate(), -// eventRequestDto.getEndDate() -// ); -// -// checkEventState(event); -// -// Event savedEvent = eventRepository.save(event); -// -// stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); -// -// return savedEvent.toDto(); -// } -// -// public Page getEvents(int page, int size) { -// Pageable pageable = PageRequest.of(page-1, size); -// Page events = eventRepository.findAll(pageable); -// -// // 모든 events들 checkState로 state 상태 갱신하기 -// events.forEach(this::checkEventState); -// -// return events.map(Event::toDto); -// } -// -// public EventResponseDto getEvent(long eventId) { -// Event event = findByIdOrElseThrow(eventId); -// -// checkEventState(event); -// -// return event.toDto(); -// } -// -// @Secured("ADMIN") -// public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { -// Event event = findByIdOrElseThrow(eventId); -// -// event.update(eventRequestDto); -// -// checkEventState(event); -// -// return event.toDto(); -// } -// -// private void checkEventState(Event event) { -// LocalDateTime now = LocalDateTime.now(); -// EventState newState = -// ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && -// (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) -// ? EventState.VALID -// : EventState.INVALID; -// -// if (event.getState() != newState) { -// event.setState(newState); -// eventRepository.save(event); -// } -// } -// -// public Event getValidEventOrThrow(Long eventId) { -// Event event = findByIdOrElseThrow(eventId); -// -// checkEventState(event); -// -// if(event.getState() != EventState.VALID) { -// throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); -// } -// -// return event; -// } -// -// public Event findByIdOrElseThrow(Long eventId) { -// return eventRepository.findById(eventId) -// .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); -// } -//} +package com.example.eightyage.domain.event.service; + +import com.example.eightyage.domain.event.dto.request.EventRequestDto; +import com.example.eightyage.domain.event.dto.response.EventResponseDto; +import com.example.eightyage.domain.event.entity.Event; +import com.example.eightyage.domain.event.entity.EventState; +import com.example.eightyage.domain.event.repository.EventRepository; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class EventService { + + private final EventRepository eventRepository; + private final StringRedisTemplate stringRedisTemplate; + + @Secured("ADMIN") + public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { + Event event = new Event( + eventRequestDto.getName(), + eventRequestDto.getDescription(), + eventRequestDto.getQuantity(), + eventRequestDto.getStartDate(), + eventRequestDto.getEndDate() + ); + + checkEventState(event); + + Event savedEvent = eventRepository.save(event); + + stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); + + return savedEvent.toDto(); + } + + public Page getEvents(int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page events = eventRepository.findAll(pageable); + + // 모든 events들 checkState로 state 상태 갱신하기 + events.forEach(this::checkEventState); + + return events.map(Event::toDto); + } + + public EventResponseDto getEvent(long eventId) { + Event event = findByIdOrElseThrow(eventId); + + checkEventState(event); + + return event.toDto(); + } + + @Secured("ADMIN") + public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { + Event event = findByIdOrElseThrow(eventId); + + event.update(eventRequestDto); + + checkEventState(event); + + return event.toDto(); + } + + private void checkEventState(Event event) { + LocalDateTime now = LocalDateTime.now(); + EventState newState = + ( (event.getStartDate().isBefore(now) || event.getStartDate().isEqual(now)) && + (event.getEndDate().isAfter(now) || event.getEndDate().isEqual(now)) ) + ? EventState.VALID + : EventState.INVALID; + + if (event.getState() != newState) { + event.setState(newState); + eventRepository.save(event); + } + } + + public Event getValidEventOrThrow(Long eventId) { + Event event = findByIdOrElseThrow(eventId); + + checkEventState(event); + + if(event.getState() != EventState.VALID) { + throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); + } + + return event; + } + + public Event findByIdOrElseThrow(Long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); + } +} From 9dea156abd59c93811442f4c208e568ab605e9aa Mon Sep 17 00:00:00 2001 From: peridot Date: Fri, 28 Mar 2025 17:45:09 +0900 Subject: [PATCH 132/164] =?UTF-8?q?fix(coupon)=20rlock=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 74 +++++++++---------- .../domain/coupon/service/CouponService.java | 13 ++-- .../event/controller/EventController.java | 3 + .../domain/event/service/EventService.java | 2 - 4 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index c1e1939..cdc3b69 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,38 +1,36 @@ -//package com.example.eightyage.domain.coupon.controller; -// -//import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -//import com.example.eightyage.domain.coupon.service.CouponService; -//import com.example.eightyage.global.dto.AuthUser; -//import lombok.RequiredArgsConstructor; -//import org.springframework.data.domain.Page; -//import org.springframework.http.ResponseEntity; -//import org.springframework.security.core.annotation.AuthenticationPrincipal; -//import org.springframework.web.bind.annotation.*; -// -//import java.util.List; -// -//@RestController -//@RequestMapping("/api") -//@RequiredArgsConstructor -//public class CouponController { -// -// private final CouponService couponService; -// -// @PostMapping("/v1/events/{eventId}/coupons") -// public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { -// return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); -// } -// -// @GetMapping("/v1/coupons/my") -// public ResponseEntity> getMyCoupons( -// @AuthenticationPrincipal AuthUser authUser, -// @RequestParam(defaultValue = "1") int page, -// @RequestParam(defaultValue = "10") int size) { -// return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); -// } -// -// @GetMapping("/v1/coupons/{couponId}") -// public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { -// return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); -// } -//} +package com.example.eightyage.domain.coupon.controller; + +import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.service.CouponService; +import com.example.eightyage.global.dto.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class CouponController { + + private final CouponService couponService; + + @PostMapping("/v1/events/{eventId}/coupons") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { + return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); + } + + @GetMapping("/v1/coupons/my") + public ResponseEntity> getMyCoupons( + @AuthenticationPrincipal AuthUser authUser, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); + } + + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { + return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index fe3219d..7f45cde 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -47,19 +47,18 @@ public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 } - // 락 획득 -> 임계 구역 진입 - // 쿠폰 수량 우선 차감 - Long remain = stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); - if (remain == 0 || remain < 0) { - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - Event event = eventService.getValidEventOrThrow(eventId); if (couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } + Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); + if (remain == 0 || remain < 0) { + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); + } + stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + // 쿠폰 발급 및 저장 Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); couponRepository.save(coupon); diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java index 9a38ff6..17c8a81 100644 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @@ -16,6 +17,7 @@ public class EventController { private final EventService eventService; + @PreAuthorize("hasRole('ADMIN')") @PostMapping("/v1/events") public ResponseEntity createEvent(@Valid @RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); @@ -31,6 +33,7 @@ public ResponseEntity getEvent(@PathVariable long eventId) { return ResponseEntity.ok(eventService.getEvent(eventId)); } + @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/v1/events/{eventId}") public ResponseEntity updateEvent(@PathVariable long eventId, @Valid @RequestBody EventRequestDto eventRequestDto) { return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index 9af0bfc..f7d38a6 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -24,7 +24,6 @@ public class EventService { private final EventRepository eventRepository; private final StringRedisTemplate stringRedisTemplate; - @Secured("ADMIN") public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { Event event = new Event( eventRequestDto.getName(), @@ -61,7 +60,6 @@ public EventResponseDto getEvent(long eventId) { return event.toDto(); } - @Secured("ADMIN") public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { Event event = findByIdOrElseThrow(eventId); From c038d7c003ca2d1bb6165debd93dcc412d641f96 Mon Sep 17 00:00:00 2001 From: peridot Date: Fri, 28 Mar 2025 17:53:28 +0900 Subject: [PATCH 133/164] =?UTF-8?q?fix(coupon)=20rlock=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/eightyage/domain/event/service/EventService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index f7d38a6..91c9b5f 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -12,7 +12,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Service; import java.time.LocalDateTime; From f97f111f7a4ce43100f10cbbfa2b2dd5bc683bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 20:51:08 +0900 Subject: [PATCH 134/164] =?UTF-8?q?refactor:=20KEY=20=EB=AA=85=EC=B9=AD=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 164064b..b7d1ee6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,23 +32,23 @@ spring: use_sql_comments: true dialect: org.hibernate.dialect.MySQLDialect - cloud: - aws: - credentials: - access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} - region: - static: ap-northeast-2 - s3: - bucket: ${S3_BUCKET} - -aws: - credentials: - access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} - region: ap-northeast-2 - s3: - bucket: ${S3_BUCKET} +# cloud: +# aws: +# credentials: +# access-key: ${S3_ACCESS_KEY} +# secret-key: ${S3_SECRET_KEY} +# region: +# static: ap-northeast-2 +# s3: +# bucket: ${S3_BUCKET} +# +#aws: +# credentials: +# access-key: ${S3_ACCESS_KEY} +# secret-key: ${S3_SECRET_KEY} +# region: ap-northeast-2 +# s3: +# bucket: ${S3_BUCKET} data: redis: From 337b1c852cbfb7cc1c4c1aa0256a22c018585094 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Fri, 28 Mar 2025 21:00:35 +0900 Subject: [PATCH 135/164] =?UTF-8?q?refactor(search):=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/eightyage/domain/search/dto/PopularKeywordDto.java | 2 +- .../com/example/eightyage/domain/search/entity/SearchLog.java | 2 +- .../eightyage/domain/search/service/v1/SearchServiceV1.java | 2 +- .../eightyage/domain/search/service/v2/SearchServiceV2.java | 2 +- .../domain/search/service/v3/PopularKeywordServiceV3.java | 2 +- .../eightyage/domain/search/service/v3/SearchServiceV3.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java index 89eac11..b8db82b 100644 --- a/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java +++ b/src/main/java/com/example/eightyage/domain/search/dto/PopularKeywordDto.java @@ -14,7 +14,7 @@ public class PopularKeywordDto { private String keyword; private Long count; - public static PopularKeywordDto of(String keyword, Long score) { + public static PopularKeywordDto keywordOf(String keyword, Long score) { return new PopularKeywordDto(keyword, score); } } diff --git a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java index 48588b4..94981a9 100644 --- a/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java +++ b/src/main/java/com/example/eightyage/domain/search/entity/SearchLog.java @@ -27,7 +27,7 @@ public class SearchLog { @CreatedDate private LocalDateTime searchedAt; - public static SearchLog of(String keyword) { + public static SearchLog keywordOf(String keyword) { SearchLog log = new SearchLog(); log.keyword = keyword; return log; diff --git a/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java b/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java index 3241312..753104e 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v1/SearchServiceV1.java @@ -18,7 +18,7 @@ public class SearchServiceV1 { @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSearchLog(String keyword){ if(StringUtils.hasText(keyword)){ - searchLogRepository.save(SearchLog.of(keyword)); + searchLogRepository.save(SearchLog.keywordOf(keyword)); } } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index c2928a6..05f40f7 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -27,7 +27,7 @@ public class SearchServiceV2 { @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSearchLog(String keyword) { if (StringUtils.hasText(keyword)) { - searchLogRepository.save(SearchLog.of(keyword)); + searchLogRepository.save(SearchLog.keywordOf(keyword)); } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java index 843a561..95afe83 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/PopularKeywordServiceV3.java @@ -28,7 +28,7 @@ public List searchPopularKeywords(int limit) { if (keywordSet == null) { return List.of(); } - return keywordSet.stream().map(tuple -> PopularKeywordDto.of(tuple.getValue(), Objects.requireNonNull(tuple.getScore()).longValue())) + return keywordSet.stream().map(tuple -> PopularKeywordDto.keywordOf(tuple.getValue(), Objects.requireNonNull(tuple.getScore()).longValue())) .collect(Collectors.toList()); } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java index 13e04aa..c366401 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java @@ -24,7 +24,7 @@ public class SearchServiceV3 { @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSearchLog(String keyword) { if (StringUtils.hasText(keyword)) { - searchLogRepository.save(SearchLog.of(keyword)); + searchLogRepository.save(SearchLog.keywordOf(keyword)); } } From 16fabe3f17b2be51ca2c05c2529ccd31468a6fb0 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 21:14:24 +0900 Subject: [PATCH 136/164] =?UTF-8?q?refactor(global):=20deletedAt=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 것 - TimeStamped에서 deletedAt 삭제 - user와 product만 deletedAt 적용 - delete 부분 모두 수정 --- .../eightyage/domain/auth/entity/RefreshToken.java | 1 + .../eightyage/domain/auth/service/TokenService.java | 2 +- .../auth/{entity => tokenstate}/TokenState.java | 2 +- .../eightyage/domain/product/entity/Product.java | 8 ++++++++ .../product/repository/ProductImageRepository.java | 2 +- .../domain/product/service/ProductImageService.java | 2 +- .../domain/product/service/ProductService.java | 8 ++------ .../domain/review/repository/ReviewRepository.java | 4 ++-- .../domain/review/service/ReviewService.java | 6 +++--- .../example/eightyage/domain/user/entity/User.java | 5 ++++- .../global/{entity => dto}/ErrorResponse.java | 2 +- .../example/eightyage/global/entity/TimeStamped.java | 12 ------------ .../global/exception/BadRequestException.java | 2 +- .../global/exception/ForbiddenException.java | 2 +- .../global/exception/GlobalExceptionHandler.java | 6 +++--- .../{CustomException.java => HandledException.java} | 6 +++--- .../global/exception/NotFoundException.java | 2 +- .../global/exception/UnauthorizedException.java | 2 +- .../domain/auth/service/TokenServiceTest.java | 4 ++-- 19 files changed, 37 insertions(+), 41 deletions(-) rename src/main/java/com/example/eightyage/domain/auth/{entity => tokenstate}/TokenState.java (50%) rename src/main/java/com/example/eightyage/global/{entity => dto}/ErrorResponse.java (92%) rename src/main/java/com/example/eightyage/global/exception/{CustomException.java => HandledException.java} (76%) diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java index 0c98161..fb481c2 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java +++ b/src/main/java/com/example/eightyage/domain/auth/entity/RefreshToken.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.auth.entity; +import com.example.eightyage.domain.auth.tokenstate.TokenState; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java index b2c3514..ee0c61e 100644 --- a/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java +++ b/src/main/java/com/example/eightyage/domain/auth/service/TokenService.java @@ -10,7 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.domain.auth.tokenstate.TokenState.INVALIDATED; import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; diff --git a/src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java b/src/main/java/com/example/eightyage/domain/auth/tokenstate/TokenState.java similarity index 50% rename from src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java rename to src/main/java/com/example/eightyage/domain/auth/tokenstate/TokenState.java index 3ec0bd7..c2f358c 100644 --- a/src/main/java/com/example/eightyage/domain/auth/entity/TokenState.java +++ b/src/main/java/com/example/eightyage/domain/auth/tokenstate/TokenState.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.auth.entity; +package com.example.eightyage.domain.auth.tokenstate; public enum TokenState { VALID, diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 169b980..5f061f9 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -37,6 +38,9 @@ public class Product extends TimeStamped { @OneToMany(mappedBy = "product") private List reviews = new ArrayList<>(); + @Temporal(TemporalType.TIMESTAMP) + private LocalDateTime deletedAt; + @Builder public Product(String name, Category category, String content, Integer price, SaleState saleState) { this.name = name; @@ -75,4 +79,8 @@ public void updateSaleState(SaleState newSaleState) { this.saleState = newSaleState; } } + + public void deleteProduct() { + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java index a29cc6c..a167757 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java @@ -10,6 +10,6 @@ public interface ProductImageRepository extends JpaRepository { - @Query("SELECT pi FROM ProductImage pi WHERE pi.id = :imageId AND pi.deletedAt IS NULL") + @Query("SELECT pi FROM ProductImage pi WHERE pi.id = :imageId") Optional findById(Long imageId); } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java index 2fb20bc..d5ed35e 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -68,7 +68,7 @@ public String uploadImage(Long productId, MultipartFile file) { public void deleteImage(Long imageId) { ProductImage findProductImage = findProductImageByIdOrElseThrow(imageId); - findProductImage.delete(); + productImageRepository.delete(findProductImage); } public ProductImage findProductImageByIdOrElseThrow(Long imageId){ diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 9d4e986..052a4a0 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -138,11 +138,9 @@ public void deleteProduct(Long productId) { Product findProduct = findProductByIdOrElseThrow(productId); List findReviewList = reviewRepository.findReviewsByProductId(productId); - for (Review review : findReviewList) { - review.delete(); - } + reviewRepository.deleteAll(findReviewList); - findProduct.delete(); + findProduct.deleteProduct(); } public Product findProductByIdOrElseThrow(Long productId) { @@ -150,6 +148,4 @@ public Product findProductByIdOrElseThrow(Long productId) { () -> new NotFoundException("해당 제품이 존재하지 않습니다.") ); } - - } diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java index 24716be..fd3bd04 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewRepository.java @@ -17,11 +17,11 @@ @Repository public interface ReviewRepository extends JpaRepository { - @Query("SELECT r FROM Review r WHERE r.id = :reviewId AND r.deletedAt IS NULL") + @Query("SELECT r FROM Review r WHERE r.id = :reviewId") Optional findById(@Param("reviewId") Long reviewId); Page findByProductIdAndProductDeletedAtIsNull(Long productId, Pageable pageable); - @Query("SELECT r FROM Review r JOIN FETCH r.user JOIN FETCH r.product WHERE r.product.id = :productId AND r.deletedAt IS NULL") + @Query("SELECT r FROM Review r JOIN FETCH r.user JOIN FETCH r.product WHERE r.product.id = :productId") List findReviewsByProductId(@Param("productId") Long productId); } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 5230b08..3d025ef 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -100,11 +100,11 @@ public void deleteReview(Long userId, Long reviewId) { User findUser = userService.findUserByIdOrElseThrow(userId); Review findReview = findReviewByIdOrElseThrow(reviewId); - if(findUser.getId().equals(findReview.getUser().getId())){ - findReview.delete(); - } else { + if(!findUser.getId().equals(findReview.getUser().getId())){ throw new UnauthorizedException("리뷰를 삭제할 권한이 없습니다."); } + + reviewRepository.delete(findReview); } public Review findReviewByIdOrElseThrow(Long reviewId){ diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index f208901..5847eec 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -29,6 +29,9 @@ public class User extends TimeStamped { @Enumerated(EnumType.STRING) private UserRole userRole; + @Temporal(TemporalType.TIMESTAMP) + private LocalDateTime deletedAt; + @Builder public User(Long id, String email, String nickname, String password, UserRole userRole) { this.id = id; @@ -48,6 +51,6 @@ public static User fromAuthUser(AuthUser authUser) { } public void deleteUser() { - setDeletedAt(LocalDateTime.now()); + this.deletedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java b/src/main/java/com/example/eightyage/global/dto/ErrorResponse.java similarity index 92% rename from src/main/java/com/example/eightyage/global/entity/ErrorResponse.java rename to src/main/java/com/example/eightyage/global/dto/ErrorResponse.java index 6d9a51d..37382de 100644 --- a/src/main/java/com/example/eightyage/global/entity/ErrorResponse.java +++ b/src/main/java/com/example/eightyage/global/dto/ErrorResponse.java @@ -1,4 +1,4 @@ -package com.example.eightyage.global.entity; +package com.example.eightyage.global.dto; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java index 27b911b..75063c1 100644 --- a/src/main/java/com/example/eightyage/global/entity/TimeStamped.java +++ b/src/main/java/com/example/eightyage/global/entity/TimeStamped.java @@ -23,16 +23,4 @@ public abstract class TimeStamped { @Column @Temporal(TemporalType.TIMESTAMP) private LocalDateTime modifiedAt; - - @Column - @Temporal(TemporalType.TIMESTAMP) - private LocalDateTime deletedAt; - - public void delete() { - this.deletedAt = LocalDateTime.now(); - } - - public void setDeletedAt(LocalDateTime deletedAt) { - this.deletedAt = deletedAt; - } } diff --git a/src/main/java/com/example/eightyage/global/exception/BadRequestException.java b/src/main/java/com/example/eightyage/global/exception/BadRequestException.java index e6ae476..ef8afb4 100644 --- a/src/main/java/com/example/eightyage/global/exception/BadRequestException.java +++ b/src/main/java/com/example/eightyage/global/exception/BadRequestException.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.exception; -public class BadRequestException extends CustomException { +public class BadRequestException extends HandledException { public BadRequestException() { super(ErrorCode.BAD_REQUEST); diff --git a/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java b/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java index 3cd553e..9d500a9 100644 --- a/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java +++ b/src/main/java/com/example/eightyage/global/exception/ForbiddenException.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.exception; -public class ForbiddenException extends CustomException { +public class ForbiddenException extends HandledException { public ForbiddenException() { super(ErrorCode.FORBIDDEN); diff --git a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java index 62af117..1afcdac 100644 --- a/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eightyage/global/exception/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.exception; -import com.example.eightyage.global.entity.ErrorResponse; +import com.example.eightyage.global.dto.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -20,8 +20,8 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(CustomException.class) - public ResponseEntity> invalidRequestExceptionException(CustomException ex) { + @ExceptionHandler(HandledException.class) + public ResponseEntity> invalidRequestExceptionException(HandledException ex) { HttpStatus httpStatus = ex.getHttpStatus(); return new ResponseEntity<>(ErrorResponse.of(httpStatus, ex.getMessage()), ex.getHttpStatus()); } diff --git a/src/main/java/com/example/eightyage/global/exception/CustomException.java b/src/main/java/com/example/eightyage/global/exception/HandledException.java similarity index 76% rename from src/main/java/com/example/eightyage/global/exception/CustomException.java rename to src/main/java/com/example/eightyage/global/exception/HandledException.java index 1bc3086..ae7ce25 100644 --- a/src/main/java/com/example/eightyage/global/exception/CustomException.java +++ b/src/main/java/com/example/eightyage/global/exception/HandledException.java @@ -4,20 +4,20 @@ import org.springframework.http.HttpStatus; @Getter -public class CustomException extends RuntimeException { +public class HandledException extends RuntimeException { private final HttpStatus httpStatus; private final String message; // 예외 던질시 기본 메세지 출력 - public CustomException(ErrorCode errorCode) { + public HandledException(ErrorCode errorCode) { super(errorCode.getDefaultMessage()); this.httpStatus = errorCode.getStatus(); this.message = errorCode.getDefaultMessage(); } // 예외 던질시 메세지 출력 - public CustomException(ErrorCode errorCode, String message) { + public HandledException(ErrorCode errorCode, String message) { super(message); this.httpStatus = errorCode.getStatus(); this.message = message; diff --git a/src/main/java/com/example/eightyage/global/exception/NotFoundException.java b/src/main/java/com/example/eightyage/global/exception/NotFoundException.java index 9233a87..d3a2cc9 100644 --- a/src/main/java/com/example/eightyage/global/exception/NotFoundException.java +++ b/src/main/java/com/example/eightyage/global/exception/NotFoundException.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.exception; -public class NotFoundException extends CustomException { +public class NotFoundException extends HandledException { public NotFoundException() { super(ErrorCode.NOT_FOUND); } diff --git a/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java b/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java index a347954..2409f33 100644 --- a/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java +++ b/src/main/java/com/example/eightyage/global/exception/UnauthorizedException.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.exception; -public class UnauthorizedException extends CustomException { +public class UnauthorizedException extends HandledException { public UnauthorizedException() { super(ErrorCode.AUTHORIZATION); diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java index 8e9492f..584784f 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -17,7 +17,7 @@ import java.util.Optional; -import static com.example.eightyage.domain.auth.entity.TokenState.INVALIDATED; +import static com.example.eightyage.domain.auth.tokenstate.TokenState.INVALIDATED; import static com.example.eightyage.global.exception.ErrorMessage.EXPIRED_REFRESH_TOKEN; import static com.example.eightyage.global.exception.ErrorMessage.REFRESH_TOKEN_NOT_FOUND; import static org.junit.jupiter.api.Assertions.*; @@ -120,7 +120,7 @@ public void setUp() { RefreshToken mockRefreshToken = mock(RefreshToken.class); given(refreshTokenRepository.findByToken(any(String.class))).willReturn(Optional.of(mockRefreshToken)); - given(userService.findUserByIdOrElseThrow(mockRefreshToken.getUserId())).willReturn(user); + given(userService.findUserByIdOrElseThrow(anyLong())).willReturn(user); // when User result = tokenService.reissueToken(refreshToken); From 1d1f14fefb72d4a7e8f75323859ad7fd92ae1eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 21:46:18 +0900 Subject: [PATCH 137/164] =?UTF-8?q?refactor(product):=20update=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20ifNotNull=20=EB=B6=99=EC=97=AC=EC=84=9C=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 --- .../eightyage/domain/product/entity/Product.java | 10 +++++----- .../domain/product/service/ProductService.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index bf19b02..14890b4 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -52,31 +52,31 @@ public Product(String name, Category category, String content, Integer price, Sa this.saleState = saleState; } - public void updateName(String newName){ + public void updateNameIfNotNull(String newName){ if(newName != null){ this.name = newName; } } - public void updateCategory(Category newCategory) { + public void updateCategoryIfNotNull(Category newCategory) { if (newCategory != null) { this.category = newCategory; } } - public void updateContent(String newContent) { + public void updateContentIfNotNull(String newContent) { if (newContent != null) { this.content = newContent; } } - public void updatePrice(Integer newPrice) { + public void updatePriceIfNotNull(Integer newPrice) { if (newPrice != null) { this.price = newPrice; } } - public void updateSaleState(SaleState newSaleState) { + public void updateSaleStateIfNotNull(SaleState newSaleState) { if (newSaleState != null) { this.saleState = newSaleState; } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 052a4a0..cd5465c 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -57,11 +57,11 @@ public ProductSaveResponseDto saveProduct(ProductSaveRequestDto requestDto) { public ProductUpdateResponseDto updateProduct(Long productId, ProductUpdateRequestDto requestDto) { Product findProduct = findProductByIdOrElseThrow(productId); - findProduct.updateName(requestDto.getProductName()); - findProduct.updateCategory(requestDto.getCategory()); - findProduct.updateContent(requestDto.getContent()); - findProduct.updateSaleState(requestDto.getSaleState()); - findProduct.updatePrice(requestDto.getPrice()); + findProduct.updateNameIfNotNull(requestDto.getProductName()); + findProduct.updateCategoryIfNotNull(requestDto.getCategory()); + findProduct.updateContentIfNotNull(requestDto.getContent()); + findProduct.updateSaleStateIfNotNull(requestDto.getSaleState()); + findProduct.updatePriceIfNotNull(requestDto.getPrice()); return ProductUpdateResponseDto.builder() .productName(findProduct.getName()) From 609fb78099595b8f9fc1d498b4d97c1e7665b5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 21:46:30 +0900 Subject: [PATCH 138/164] =?UTF-8?q?refactor(review):=20update=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20ifNotNull=20=EB=B6=99=EC=97=AC=EC=84=9C=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 --- .../com/example/eightyage/domain/review/entity/Review.java | 4 ++-- .../eightyage/domain/review/service/ReviewService.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/entity/Review.java b/src/main/java/com/example/eightyage/domain/review/entity/Review.java index dd690cd..3af3698 100644 --- a/src/main/java/com/example/eightyage/domain/review/entity/Review.java +++ b/src/main/java/com/example/eightyage/domain/review/entity/Review.java @@ -39,13 +39,13 @@ public Review(User user, Product product, Double score, String content) { this.content = content; } - public void updateScore(Double newScore){ + public void updateScoreIfNotNull(Double newScore){ if(newScore != null){ this.score = newScore; } } - public void updateContent(String newContent){ + public void updateContentIfNotNull(String newContent){ if(newContent != null){ this.content = newContent; } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 3d025ef..6039934 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -61,8 +61,8 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, ReviewUp Review findReview = findReviewByIdOrElseThrow(reviewId); if(findUser.getId().equals(findReview.getUser().getId())){ - findReview.updateScore(requestDto.getScore()); - findReview.updateContent(requestDto.getContent()); + findReview.updateScoreIfNotNull(requestDto.getScore()); + findReview.updateContentIfNotNull(requestDto.getContent()); } else { throw new UnauthorizedException("리뷰를 수정할 권한이 없습니다."); } From e580188545d86e4390dcaa04fffba1cb41ad2d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 21:50:24 +0900 Subject: [PATCH 139/164] =?UTF-8?q?refactor(product):=20find,=20get=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/controller/ProductController.java | 4 ++-- .../eightyage/domain/product/service/ProductService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index 54c28ed..fe87fc7 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -46,8 +46,8 @@ public ResponseEntity updateProduct( // 제품 단건 조회 @GetMapping("/v1/products/{productId}") - public ResponseEntity getProduct(@PathVariable Long productId){ - ProductGetResponseDto responseDto = productService.findProductById(productId); + public ResponseEntity findProduct(@PathVariable Long productId){ + ProductGetResponseDto responseDto = productService.getProductById(productId); return ResponseEntity.ok(responseDto); } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index cd5465c..5b8732e 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -76,7 +76,7 @@ public ProductUpdateResponseDto updateProduct(Long productId, ProductUpdateReque // 제품 단건 조회 @Transactional(readOnly = true) - public ProductGetResponseDto findProductById(Long productId) { + public ProductGetResponseDto getProductById(Long productId) { Product findProduct = findProductByIdOrElseThrow(productId); return ProductGetResponseDto.builder() From c9e2a674354cce7ad70aca0631196fc6d05df14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 21:50:31 +0900 Subject: [PATCH 140/164] =?UTF-8?q?refactor(review):=20find,=20get=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/controller/ReviewController.java | 4 ++-- .../eightyage/domain/review/service/ReviewService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index bdab2e7..e5cbea2 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -55,7 +55,7 @@ public ResponseEntity updateReview( // 리뷰 다건 조회 @GetMapping("/v1/products/{productId}/reviews") - public ResponseEntity> getReviews( + public ResponseEntity> findReviews( @PathVariable Long productId, @RequestParam(required = false, defaultValue = "score") String orderBy, @RequestParam(defaultValue = "0") int page, @@ -63,7 +63,7 @@ public ResponseEntity> getReviews( ){ PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, orderBy)); - Page reviews = reviewService.findReviews(productId, pageRequest); + Page reviews = reviewService.getReviews(productId, pageRequest); return ResponseEntity.ok(reviews); } diff --git a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java index 6039934..f7616b2 100644 --- a/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/eightyage/domain/review/service/ReviewService.java @@ -80,7 +80,7 @@ public ReviewUpdateResponseDto updateReview(Long userId, Long reviewId, ReviewUp // 리뷰 다건 조회 @Transactional(readOnly = true) - public Page findReviews(Long productId, PageRequest pageRequest) { + public Page getReviews(Long productId, PageRequest pageRequest) { Page reviewPage = reviewRepository.findByProductIdAndProductDeletedAtIsNull(productId, pageRequest); return reviewPage.map(review -> ReviewsGetResponseDto.builder() From 96b5528d0e54baca7b0cf40dec3689d04897a396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 21:53:54 +0900 Subject: [PATCH 141/164] =?UTF-8?q?refactor(product):=20category,=20salest?= =?UTF-8?q?ate=20enums=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=94=B0=EB=A1=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/{entity => category}/Category.java | 2 +- .../domain/product/controller/ProductController.java | 2 +- .../domain/product/dto/request/ProductSaveRequestDto.java | 2 +- .../product/dto/request/ProductUpdateRequestDto.java | 4 ++-- .../domain/product/dto/response/ProductGetResponseDto.java | 4 ++-- .../product/dto/response/ProductSaveResponseDto.java | 4 ++-- .../product/dto/response/ProductSearchResponseDto.java | 2 +- .../product/dto/response/ProductUpdateResponseDto.java | 4 ++-- .../example/eightyage/domain/product/entity/Product.java | 2 ++ .../example/eightyage/domain/product/entity/SaleState.java | 6 ------ .../domain/product/repository/ProductBulkRepository.java | 4 ++-- .../domain/product/repository/ProductRepository.java | 2 +- .../eightyage/domain/product/salestate/SaleState.java | 6 ++++++ .../eightyage/domain/product/service/ProductService.java | 4 ++-- .../domain/product/service/ProductServiceTest.java | 7 ++----- 15 files changed, 27 insertions(+), 28 deletions(-) rename src/main/java/com/example/eightyage/domain/product/{entity => category}/Category.java (90%) delete mode 100644 src/main/java/com/example/eightyage/domain/product/entity/SaleState.java create mode 100644 src/main/java/com/example/eightyage/domain/product/salestate/SaleState.java diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Category.java b/src/main/java/com/example/eightyage/domain/product/category/Category.java similarity index 90% rename from src/main/java/com/example/eightyage/domain/product/entity/Category.java rename to src/main/java/com/example/eightyage/domain/product/category/Category.java index 044799d..12c9345 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Category.java +++ b/src/main/java/com/example/eightyage/domain/product/category/Category.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.product.entity; +package com.example.eightyage.domain.product.category; public enum Category { SKINCARE("스킨케어"), diff --git a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java index fe87fc7..f44cbd4 100644 --- a/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java +++ b/src/main/java/com/example/eightyage/domain/product/controller/ProductController.java @@ -6,7 +6,7 @@ import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.service.ProductService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java index 9cb5616..a7cd103 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductSaveRequestDto.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.product.dto.request; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java index c7bccbd..ec72ff0 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -1,7 +1,7 @@ package com.example.eightyage.domain.product.dto.request; -import com.example.eightyage.domain.product.entity.Category; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.salestate.SaleState; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java index 4b76fd7..b2fb059 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java @@ -1,7 +1,7 @@ package com.example.eightyage.domain.product.dto.response; -import com.example.eightyage.domain.product.entity.Category; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.salestate.SaleState; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java index 552387e..23fb6d6 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java @@ -1,7 +1,7 @@ package com.example.eightyage.domain.product.dto.response; -import com.example.eightyage.domain.product.entity.Category; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.salestate.SaleState; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java index d52d526..3205518 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSearchResponseDto.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.product.dto.response; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java index a2bf8f4..62ab718 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java @@ -1,7 +1,7 @@ package com.example.eightyage.domain.product.dto.response; -import com.example.eightyage.domain.product.entity.Category; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.salestate.SaleState; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 14890b4..4c09ac5 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -1,5 +1,7 @@ package com.example.eightyage.domain.product.entity; +import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.salestate.SaleState; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; diff --git a/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java b/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java deleted file mode 100644 index e69c789..0000000 --- a/src/main/java/com/example/eightyage/domain/product/entity/SaleState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.product.entity; - -public enum SaleState { - FOR_SALE, - SOLD_OUT -} diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java index 272b3e8..e9818db 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java @@ -1,8 +1,8 @@ package com.example.eightyage.domain.product.repository; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.salestate.SaleState; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java index 6158523..90baefc 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductRepository.java @@ -1,7 +1,7 @@ package com.example.eightyage.domain.product.repository; import com.example.eightyage.domain.product.dto.response.ProductSearchResponseDto; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/example/eightyage/domain/product/salestate/SaleState.java b/src/main/java/com/example/eightyage/domain/product/salestate/SaleState.java new file mode 100644 index 0000000..7445bbb --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/product/salestate/SaleState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.product.salestate; + +public enum SaleState { + FOR_SALE, + SOLD_OUT +} diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 5b8732e..01a81a4 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -3,9 +3,9 @@ import com.example.eightyage.domain.product.dto.request.ProductSaveRequestDto; import com.example.eightyage.domain.product.dto.request.ProductUpdateRequestDto; import com.example.eightyage.domain.product.dto.response.*; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.salestate.SaleState; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java index 811dc93..da23f05 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -5,14 +5,12 @@ import com.example.eightyage.domain.product.dto.response.ProductGetResponseDto; import com.example.eightyage.domain.product.dto.response.ProductSaveResponseDto; import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; -import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.salestate.SaleState; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -24,7 +22,6 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; From c41b4a0596ba7be9190b8182a54e95b1c01676f3 Mon Sep 17 00:00:00 2001 From: Seoyeon Date: Fri, 28 Mar 2025 21:56:03 +0900 Subject: [PATCH 142/164] =?UTF-8?q?refactor(search):=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20DB=20=EC=A0=9C=EA=B1=B0=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/service/ProductService.java | 8 +- .../domain/search/entity/KeywordCount.java | 25 ------- .../repository/KeywordCountRepository.java | 8 -- .../service/v2/KeywordCountFlushService.java | 73 ------------------- .../service/v2/PopularKeywordServiceV2.java | 4 +- .../search/service/v2/SearchServiceV2.java | 37 ---------- .../search/service/v3/SearchServiceV3.java | 2 +- .../eightyage/global/config/CacheConfig.java | 2 - 8 files changed, 6 insertions(+), 153 deletions(-) delete mode 100644 src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java delete mode 100644 src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 9d4e986..d8563a1 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -98,7 +98,7 @@ public Page getProductsV1(String productName, Category Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); if (StringUtils.hasText(productName) && !productsResponse.isEmpty()) { - searchServiceV1.saveSearchLog(productName); // 로그만 저장 + searchServiceV1.saveSearchLog(productName); // 로그 저장 } return productsResponse; } @@ -112,7 +112,6 @@ public Page getProductsV2(String productName, Category if (StringUtils.hasText(productName) && !productsResponse.isEmpty()) { searchServiceV2.saveSearchLog(productName); // 로그 저장 - searchServiceV2.increaseKeywordCount(productName); // 캐시 작업 } return productsResponse; @@ -126,8 +125,8 @@ public Page getProductsV3(String productName, Category Page productsResponse = productRepository.findProductsOrderByReviewScore(productName, category, pageable); if(StringUtils.hasText(productName) && !productsResponse.isEmpty()){ - searchServiceV3.saveSearchLog(productName); - searchServiceV3.increaseSortedKeywordRank(productName); + searchServiceV3.saveSearchLog(productName); // 로그 저장 + searchServiceV3.increaseSortedKeywordRank(productName); // 캐시 추가 } return productsResponse; } @@ -151,5 +150,4 @@ public Product findProductByIdOrElseThrow(Long productId) { ); } - } diff --git a/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java b/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java deleted file mode 100644 index 6ceada2..0000000 --- a/src/main/java/com/example/eightyage/domain/search/entity/KeywordCount.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.eightyage.domain.search.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class KeywordCount { - - @Id - private String keyword; - private Long count; - - public KeywordCount(String keyword, Long count) { - this.keyword = keyword; - this.count = count; - } - - public void updateCount(Long count) { - this.count = count; - } -} diff --git a/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java b/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java deleted file mode 100644 index 0341ab8..0000000 --- a/src/main/java/com/example/eightyage/domain/search/repository/KeywordCountRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.eightyage.domain.search.repository; - - -import com.example.eightyage.domain.search.entity.KeywordCount; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface KeywordCountRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java b/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java deleted file mode 100644 index b7aa890..0000000 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/KeywordCountFlushService.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.example.eightyage.domain.search.service.v2; - -import com.example.eightyage.domain.search.entity.KeywordCount; -import com.example.eightyage.domain.search.repository.KeywordCountRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Set; - -@Service -@RequiredArgsConstructor -@Slf4j -public class KeywordCountFlushService { - - private final CacheManager cacheManager; - private final KeywordCountRepository keywordCountRepository; - private static final String KEYWORD_COUNT_MAP = "keywordCountMap"; - private static final String KEYWORD_KEY_SET = "keywordKeySet"; - - @Transactional - @Scheduled(fixedRate = 5 * 60 * 1000) // 5분마다 실행 - public void flushKeywordCounts() { - Cache countCache = cacheManager.getCache(KEYWORD_COUNT_MAP); - Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); - - if (countCache == null || keySetCache == null) { - log.warn("캐시를 찾을 수 없습니다."); - return; - } - - try { - // 키 목록 가져오기 - Set keywordSet = keySetCache.get("keywords", Set.class); - if (keywordSet == null || keywordSet.isEmpty()) { - log.info("flush 할 키워드가 없습니다."); - return; - } - - int flushed = 0; - - // 반복문을 이용하여 저장하기 - for (String keyword : keywordSet) { - String countStr = countCache.get(keyword, String.class); - if (countStr == null) continue; - - Long count = Long.parseLong(countStr); - if (count == 0L) continue; - - keywordCountRepository.findById(keyword) - .ifPresentOrElse( - exist -> exist.updateCount(exist.getCount() + count), - () -> keywordCountRepository.save(new KeywordCount(keyword, count)) - ); - flushed++; - countCache.evict(keyword); - } - - keySetCache.put("keywords", new java.util.HashSet<>()); - - log.info("{}개의 키워드 플러시 성공", flushed); - - } catch (Exception e) { - log.error("플러시 실패", e); - } - - } - -} diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java index 7b0be98..83e2519 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/PopularKeywordServiceV2.java @@ -19,11 +19,11 @@ public class PopularKeywordServiceV2 implements PopularKeywordService { private final SearchLogRepository searchLogRepository; private static final int MIN_DAYS = 1; private static final int MAX_DAYS = 365; - + private static final String POPULAR_KEYWORDS = "popularKeywords"; //캐시O 인기 검색어 조회 @Transactional(readOnly = true) - @Cacheable(value = "popularKeywords", key = "#days") + @Cacheable(value = POPULAR_KEYWORDS, key = "#days") public List searchPopularKeywords(int days) { if (days < MIN_DAYS || days > MAX_DAYS) { throw new BadRequestException("조회 일 수는 1~365 사이여야 합니다."); diff --git a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java index 05f40f7..333332c 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v2/SearchServiceV2.java @@ -3,25 +3,17 @@ import com.example.eightyage.domain.search.entity.SearchLog; import com.example.eightyage.domain.search.repository.SearchLogRepository; import lombok.RequiredArgsConstructor; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import java.util.HashSet; -import java.util.Set; @Service @RequiredArgsConstructor public class SearchServiceV2 { private final SearchLogRepository searchLogRepository; - private final CacheManager cacheManager; - private static final String KEYWORD_COUNT_MAP = "keywordCountMap"; - private static final String KEYWORD_KEY_SET = "keywordKeySet"; - // 검색 키워드를 로그에 저장 @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -30,33 +22,4 @@ public void saveSearchLog(String keyword) { searchLogRepository.save(SearchLog.keywordOf(keyword)); } } - - // 검색 시 키워드 카운트 증가 - @Transactional - public void increaseKeywordCount(String keyword) { - if (!StringUtils.hasText(keyword)) return; - - Cache countCache = cacheManager.getCache(KEYWORD_COUNT_MAP); - - if (countCache != null) { - String countStr = countCache.get(keyword, String.class); - long count = (countStr == null) ? 1L : Long.parseLong(countStr) + 1; - countCache.put(keyword, Long.toString(count)); - } - - updateKeywordSet(keyword); - } - - // 캐시에 키워드 추가 - private void updateKeywordSet(String keyword) { - Cache keySetCache = cacheManager.getCache(KEYWORD_KEY_SET); - if (keySetCache != null) { - Set keywordSet = keySetCache.get("keywords", Set.class); - if (keywordSet == null) { - keywordSet = new HashSet<>(); - } - keywordSet.add(keyword); - keySetCache.put("keywords", keywordSet); - } - } } diff --git a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java index c366401..384d11c 100644 --- a/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java +++ b/src/main/java/com/example/eightyage/domain/search/service/v3/SearchServiceV3.java @@ -31,6 +31,6 @@ public void saveSearchLog(String keyword) { // 검색어 점수 증가 public void increaseSortedKeywordRank(String productName) { redisTemplate.opsForZSet().incrementScore(RANKING_KEY, productName, 1); - redisTemplate.expire(RANKING_KEY, Duration.ofMinutes(1)); + redisTemplate.expire(RANKING_KEY, Duration.ofMinutes(5)); } } diff --git a/src/main/java/com/example/eightyage/global/config/CacheConfig.java b/src/main/java/com/example/eightyage/global/config/CacheConfig.java index dc34521..7d3ed20 100644 --- a/src/main/java/com/example/eightyage/global/config/CacheConfig.java +++ b/src/main/java/com/example/eightyage/global/config/CacheConfig.java @@ -28,8 +28,6 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) // 캐시 별로 TTL 설정 Map configMap = new HashMap<>(); - configMap.put("keywordCountMap", defaultConfig.entryTtl(Duration.ZERO)); - configMap.put("keywordKeySet", defaultConfig.entryTtl(Duration.ZERO)); configMap.put("popularKeywords", defaultConfig.entryTtl(Duration.ofMinutes(5))); return RedisCacheManager.builder(redisConnectionFactory) From f7800fb482a2da8f72ceab487070685ab35a23a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 22:00:59 +0900 Subject: [PATCH 143/164] =?UTF-8?q?feat(review):=20findReviews=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=EC=84=B1=20=EC=9E=88=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/controller/ReviewController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java index e5cbea2..ef1d780 100644 --- a/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/eightyage/domain/review/controller/ReviewController.java @@ -54,9 +54,9 @@ public ResponseEntity updateReview( } // 리뷰 다건 조회 - @GetMapping("/v1/products/{productId}/reviews") + @GetMapping("/v1/reviews") public ResponseEntity> findReviews( - @PathVariable Long productId, + @RequestParam(required = true) Long productId, @RequestParam(required = false, defaultValue = "score") String orderBy, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size From 843c9aaa8e17daec27cca9187518314e7b3d5b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 22:05:14 +0900 Subject: [PATCH 144/164] =?UTF-8?q?refactor(product):=20=EA=B3=B5=EB=B0=B1?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/request/ProductUpdateRequestDto.java | 5 ----- .../domain/product/dto/response/ProductGetResponseDto.java | 7 ------- .../product/dto/response/ProductSaveResponseDto.java | 7 ------- .../product/dto/response/ProductUpdateResponseDto.java | 7 ------- 4 files changed, 26 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java index ec72ff0..272fccc 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/request/ProductUpdateRequestDto.java @@ -8,14 +8,9 @@ @Getter @AllArgsConstructor public class ProductUpdateRequestDto { - private String productName; - private Category category; - private String content; - private SaleState saleState; - private Integer price; } diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java index b2fb059..534ae84 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java @@ -12,18 +12,11 @@ @Builder @AllArgsConstructor public class ProductGetResponseDto { - private final String productName; - private final String content; - private final Category category; - private final Integer price; - private final SaleState saleState; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java index 23fb6d6..86abc5d 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductSaveResponseDto.java @@ -12,18 +12,11 @@ @Builder @AllArgsConstructor public class ProductSaveResponseDto { - private final String productName; - private final Category category; - private final Integer price; - private final String content; - private final SaleState saleState; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java index 62ab718..d44aea6 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductUpdateResponseDto.java @@ -12,18 +12,11 @@ @Builder @AllArgsConstructor public class ProductUpdateResponseDto { - private final String productName; - private final Integer price; - private final String content; - private final Category category; - private final SaleState saleState; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } From b787710b743d741234bbce5094d40f2869adf9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Fri, 28 Mar 2025 22:05:27 +0900 Subject: [PATCH 145/164] =?UTF-8?q?refactor(review):=20=EA=B3=B5=EB=B0=B1?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/dto/request/ReviewUpdateRequestDto.java | 2 -- .../domain/review/dto/response/ReviewSaveResponseDto.java | 8 -------- .../review/dto/response/ReviewUpdateResponseDto.java | 7 ------- .../domain/review/dto/response/ReviewsGetResponseDto.java | 7 ------- 4 files changed, 24 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java index 6fa8ef6..5f573ac 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -7,8 +7,6 @@ @Getter @AllArgsConstructor public class ReviewUpdateRequestDto { - private Double score; - private String content; } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java index 03febd7..043cca4 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewSaveResponseDto.java @@ -10,20 +10,12 @@ @Builder @AllArgsConstructor public class ReviewSaveResponseDto { - private final Long id; - private final Long userId; - private final Long productId; - private final String nickname; - private final Double score; - private final String content; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java index 9c387e5..6350d3e 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewUpdateResponseDto.java @@ -10,18 +10,11 @@ @Builder @AllArgsConstructor public class ReviewUpdateResponseDto { - private final Long id; - private final Long userId; - private final String nickname; - private final Double score; - private final String content; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } \ No newline at end of file diff --git a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java index c8742ce..9ee2dff 100644 --- a/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/review/dto/response/ReviewsGetResponseDto.java @@ -10,18 +10,11 @@ @Builder @AllArgsConstructor public class ReviewsGetResponseDto { - private final Long id; - private final Long userId; - private final String nickname; - private final Double score; - private final String content; - private final LocalDateTime createdAt; - private final LocalDateTime modifiedAt; } From b89244010b6c0c1a709f98a429af705998056e34 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 22:39:51 +0900 Subject: [PATCH 146/164] =?UTF-8?q?refactor(global):=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 것 - 더미데이터 랜덤수 생성을 리팩토링 --- .../auth/controller/AuthController.java | 4 +- .../domain/product/entity/Product.java | 4 + .../product/service/ProductService.java | 2 + .../eightyage/domain/user/entity/User.java | 1 + .../user/repository/UserBulkRepository.java | 5 +- .../domain/user/service/UserService.java | 2 +- .../user/{entity => userrole}/UserRole.java | 2 +- .../eightyage/global/dto/AuthUser.java | 2 +- .../filter/JwtAuthenticationFilter.java | 2 +- .../eightyage/global/util/JwtUtil.java | 2 +- .../eightyage/bulk/ProductBulkTest.java | 131 ++++++++++-------- .../eightyage/bulk/ReviewBulkTest.java | 121 ++++++++-------- .../example/eightyage/bulk/UserBulkTest.java | 127 ++++++++--------- .../domain/auth/service/AuthServiceTest.java | 2 +- .../domain/auth/service/TokenServiceTest.java | 2 +- .../domain/user/service/UserServiceTest.java | 2 +- src/test/resources/application-test.yml | 21 ++- 17 files changed, 235 insertions(+), 197 deletions(-) rename src/main/java/com/example/eightyage/domain/user/{entity => userrole}/UserRole.java (94%) diff --git a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java index 9ae8d48..99693ec 100644 --- a/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/eightyage/domain/auth/controller/AuthController.java @@ -13,8 +13,8 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; -import static com.example.eightyage.domain.user.entity.UserRole.Authority.ADMIN; -import static com.example.eightyage.domain.user.entity.UserRole.Authority.USER; +import static com.example.eightyage.domain.user.userrole.UserRole.Authority.ADMIN; +import static com.example.eightyage.domain.user.userrole.UserRole.Authority.USER; @RestController @RequiredArgsConstructor diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 5f061f9..10587ef 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -41,6 +41,10 @@ public class Product extends TimeStamped { @Temporal(TemporalType.TIMESTAMP) private LocalDateTime deletedAt; + public Product(Long id) { + this.id = id; + } + @Builder public Product(String name, Category category, String content, Integer price, SaleState saleState) { this.name = name; diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 052a4a0..29d4ec8 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -148,4 +148,6 @@ public Product findProductByIdOrElseThrow(Long productId) { () -> new NotFoundException("해당 제품이 존재하지 않습니다.") ); } + + } diff --git a/src/main/java/com/example/eightyage/domain/user/entity/User.java b/src/main/java/com/example/eightyage/domain/user/entity/User.java index 5847eec..c857965 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/User.java +++ b/src/main/java/com/example/eightyage/domain/user/entity/User.java @@ -1,5 +1,6 @@ package com.example.eightyage.domain.user.entity; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.entity.TimeStamped; import jakarta.persistence.*; diff --git a/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java b/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java index 2a861c0..8c953a3 100644 --- a/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java +++ b/src/main/java/com/example/eightyage/domain/user/repository/UserBulkRepository.java @@ -17,15 +17,12 @@ public class UserBulkRepository { private final int BATCH_SIZE = 1000; public void bulkInsertUsers(List users) { - String sql = "INSERT INTO user (email, password, nickname, deleted_at) values (?, ?, ?, ?)"; - - Random random = new Random(); + String sql = "INSERT INTO user (email, password, nickname) values (?, ?, ?)"; jdbcTemplate.batchUpdate(sql, users, BATCH_SIZE, (ps, argument) -> { ps.setString(1, argument.getEmail()); ps.setString(2, argument.getPassword()); ps.setString(3, argument.getNickname()); - ps.setString(4, random.nextBoolean() ? null : LocalDateTime.now().toString()); // 랜덤으로 유저 삭제 }); } } diff --git a/src/main/java/com/example/eightyage/domain/user/service/UserService.java b/src/main/java/com/example/eightyage/domain/user/service/UserService.java index fc18d0c..8840f3d 100644 --- a/src/main/java/com/example/eightyage/domain/user/service/UserService.java +++ b/src/main/java/com/example/eightyage/domain/user/service/UserService.java @@ -2,7 +2,7 @@ import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; diff --git a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java b/src/main/java/com/example/eightyage/domain/user/userrole/UserRole.java similarity index 94% rename from src/main/java/com/example/eightyage/domain/user/entity/UserRole.java rename to src/main/java/com/example/eightyage/domain/user/userrole/UserRole.java index 7f92a4f..49b1fb1 100644 --- a/src/main/java/com/example/eightyage/domain/user/entity/UserRole.java +++ b/src/main/java/com/example/eightyage/domain/user/userrole/UserRole.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.user.entity; +package com.example.eightyage.domain.user.userrole; import com.example.eightyage.global.exception.UnauthorizedException; import lombok.Getter; diff --git a/src/main/java/com/example/eightyage/global/dto/AuthUser.java b/src/main/java/com/example/eightyage/global/dto/AuthUser.java index 022a0d4..07f565e 100644 --- a/src/main/java/com/example/eightyage/global/dto/AuthUser.java +++ b/src/main/java/com/example/eightyage/global/dto/AuthUser.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.dto; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import lombok.Builder; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; diff --git a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java index 1e626ec..dccf118 100644 --- a/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/eightyage/global/filter/JwtAuthenticationFilter.java @@ -1,6 +1,6 @@ package com.example.eightyage.global.filter; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.global.config.JwtAuthenticationToken; import com.example.eightyage.global.util.JwtUtil; import com.example.eightyage.global.dto.AuthUser; diff --git a/src/main/java/com/example/eightyage/global/util/JwtUtil.java b/src/main/java/com/example/eightyage/global/util/JwtUtil.java index 8547e15..1716dd0 100644 --- a/src/main/java/com/example/eightyage/global/util/JwtUtil.java +++ b/src/main/java/com/example/eightyage/global/util/JwtUtil.java @@ -4,7 +4,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 3b2584c..99df7d3 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -1,61 +1,70 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.product.entity.Product; -//import com.example.eightyage.domain.product.repository.ProductBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.context.ActiveProfiles; -// -//import java.util.ArrayList; -//import java.util.List; -//import java.util.UUID; -// -//@SpringBootTest -//@ActiveProfiles("ci") -//public class ProductBulkTest { -// -// @Autowired -// private ProductBulkRepository productBulkRepository; -// -// @Test -// void 제품_더미데이터_생성() { -// -// int insertCount; -// -// if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 100; // CI에서는 데이터 적게 -// } else { -// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 -// } -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < insertCount; i++) { -// Product product = Product.builder() -// .name(UUID.randomUUID().toString()) -// .build(); -// batchList.add(product); -// -// if (batchList.size() == insertCount) { -// productBulkRepository.bulkInsertProduct(batchList); -// batchList.clear(); -// -// sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// productBulkRepository.bulkInsertProduct(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.product.entity.Category; +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.SaleState; +import com.example.eightyage.domain.product.repository.ProductBulkRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +@SpringBootTest +@ActiveProfiles("ci") +public class ProductBulkTest { + + @Autowired + private ProductBulkRepository productBulkRepository; + + @Test + void 제품_더미데이터_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + Random random = new Random(); + + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + Category randomCategory = Category.values()[random.nextInt(Category.values().length)]; + Product product = Product.builder() + .category(randomCategory) + .name(UUID.randomUUID().toString()) + .saleState(random.nextBoolean() ? SaleState.FOR_SALE : SaleState.SOLD_OUT) + .build(); + + batchList.add(product); + + if (batchList.size() == insertCount) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + + sleep(500); + } + } + + if (!batchList.isEmpty()) { + productBulkRepository.bulkInsertProduct(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index ebd86aa..458d2c0 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -1,58 +1,65 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.review.entity.Review; -//import com.example.eightyage.domain.review.repository.ReviewBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.context.ActiveProfiles; -// -//import java.util.ArrayList; -//import java.util.List; -// -//@SpringBootTest -//@ActiveProfiles("ci") -//public class ReviewBulkTest { -// -// @Autowired -// private ReviewBulkRepository reviewBulkRepository; -// -// @Test -// void 리뷰_더미데이터_생성() { -// -// int insertCount; -// -// if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 100; // CI에서는 데이터 적게 -// } else { -// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 -// } -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < insertCount; i++) { -// Review review = new Review(); -// batchList.add(review); -// -// if (batchList.size() == insertCount) { -// reviewBulkRepository.bulkInsertReviews(batchList); -// batchList.clear(); -// +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.review.entity.Review; +import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +import com.example.eightyage.domain.user.entity.User; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +@SpringBootTest +@ActiveProfiles("ci") +public class ReviewBulkTest { + + @Autowired + private ReviewBulkRepository reviewBulkRepository; + + @Test + void 리뷰_더미데이터_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + Random random = new Random(); + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + User user = User.builder().id(1L).build(); + Product product = new Product((long) (random.nextInt(100) + 1)); + + Review review = new Review(user, product, random.nextDouble() * 5, "content" + i); + batchList.add(review); + + if (batchList.size() == insertCount) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + // sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// reviewBulkRepository.bulkInsertReviews(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} + } + } + + if (!batchList.isEmpty()) { + reviewBulkRepository.bulkInsertReviews(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 084b16f..ae2799f 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,64 +1,65 @@ -//package com.example.eightyage.bulk; -// -//import com.example.eightyage.domain.user.entity.User; -//import com.example.eightyage.domain.user.entity.UserRole; -//import com.example.eightyage.domain.user.repository.UserBulkRepository; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.context.ActiveProfiles; -// -//import java.util.ArrayList; -//import java.util.List; -// -//@SpringBootTest -//@ActiveProfiles("ci") -//public class UserBulkTest { -// -// @Autowired -// private UserBulkRepository userBulkRepository; -// -// @Test -// void 유저_데이터_백만건_생성() { -// -// int insertCount; -// -// if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 100; // CI에서는 데이터 적게 -// } else { -// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 -// } -// -// List batchList = new ArrayList<>(); -// -// for (int i = 0; i < insertCount; i++) { -// User user = User.builder() -// .email(i + "@email.com") -// .nickname("nickname" + i) -// .password("password") -// .userRole(UserRole.ROLE_USER) -// .build(); -// batchList.add(user); -// -// if (batchList.size() == insertCount) { -// userBulkRepository.bulkInsertUsers(batchList); -// batchList.clear(); -// +package com.example.eightyage.bulk; + +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.domain.user.userrole.UserRole; +import com.example.eightyage.domain.user.repository.UserBulkRepository; +import com.example.eightyage.global.util.RandomCodeGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +@ActiveProfiles("ci") +public class UserBulkTest { + + @Autowired + private UserBulkRepository userBulkRepository; + + @Test + void 유저_데이터_생성() { + + int insertCount; + + if ("ci".equals(System.getProperty("spring.profiles.active"))) { + insertCount = 100; // CI에서는 데이터 적게 + } else { + insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 + } + + List batchList = new ArrayList<>(); + + for (int i = 0; i < insertCount; i++) { + User user = User.builder() + .email(RandomCodeGenerator.generateCouponCode(8) + "@email.com") + .nickname("nickname" + i) + .password("password") + .userRole(UserRole.ROLE_USER) + .build(); + batchList.add(user); + + if (batchList.size() == insertCount) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + // sleep(500); -// } -// } -// -// if (!batchList.isEmpty()) { -// userBulkRepository.bulkInsertUsers(batchList); -// batchList.clear(); -// } -// } -// -// private static void sleep(int millis) { -// try { -// Thread.sleep(millis); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } -// } -//} + } + } + + if (!batchList.isEmpty()) { + userBulkRepository.bulkInsertUsers(batchList); + batchList.clear(); + } + } + + private static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java index c65ea01..7254b8e 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/AuthServiceTest.java @@ -4,7 +4,7 @@ import com.example.eightyage.domain.auth.dto.request.AuthSignupRequestDto; import com.example.eightyage.domain.auth.dto.response.AuthTokensResponseDto; import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.UnauthorizedException; diff --git a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java index 584784f..455f169 100644 --- a/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/auth/service/TokenServiceTest.java @@ -3,7 +3,7 @@ import com.example.eightyage.domain.auth.entity.RefreshToken; import com.example.eightyage.domain.auth.repository.RefreshTokenRepository; import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.domain.user.service.UserService; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.UnauthorizedException; diff --git a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java index 8041ceb..e2fd5f7 100644 --- a/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/user/service/UserServiceTest.java @@ -2,7 +2,7 @@ import com.example.eightyage.domain.user.dto.request.UserDeleteRequestDto; import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index da95d4b..bb86911 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -7,10 +7,27 @@ spring: jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: show_sql: true format_sql: true use_sql_comments: true - dialect: org.hibernate.dialect.MySQLDialect \ No newline at end of file + dialect: org.hibernate.dialect.MySQLDialect + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: + static: ap-northeast-2 + s3: + bucket: my-gom-bucket + +aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + region: ap-northeast-2 + s3: + bucket: my-gom-bucket \ No newline at end of file From c2d30def3d05617a7f148b830ed479da54803a06 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 22:51:38 +0900 Subject: [PATCH 147/164] =?UTF-8?q?refactor(global):=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 것 - 더미데이터 랜덤수 생성을 리팩토링 --- src/test/java/com/example/eightyage/bulk/ProductBulkTest.java | 2 +- src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java | 2 +- src/test/java/com/example/eightyage/bulk/UserBulkTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 99df7d3..d7e35b9 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -27,7 +27,7 @@ public class ProductBulkTest { int insertCount; if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 + insertCount = 0; // CI에서는 데이터 적게 } else { insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 } diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 458d2c0..38ee354 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -26,7 +26,7 @@ public class ReviewBulkTest { int insertCount; if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 + insertCount = 0; // CI에서는 데이터 적게 } else { insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 } diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index ae2799f..4a8c987 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -25,7 +25,7 @@ public class UserBulkTest { int insertCount; if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 100; // CI에서는 데이터 적게 + insertCount = 1; // CI에서는 데이터 적게 } else { insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 } From eff7f86c0e0a0a1e066c7481be858265b3933b77 Mon Sep 17 00:00:00 2001 From: queenriwon Date: Fri, 28 Mar 2025 23:11:50 +0900 Subject: [PATCH 148/164] =?UTF-8?q?refactor(global):=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 것 - 더미데이터 랜덤수 생성을 리팩토링 --- .../eightyage/bulk/ProductBulkTest.java | 140 +++++++++--------- .../eightyage/bulk/ReviewBulkTest.java | 130 ++++++++-------- .../example/eightyage/bulk/UserBulkTest.java | 130 ++++++++-------- 3 files changed, 200 insertions(+), 200 deletions(-) diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index d7e35b9..3d8c7db 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -1,70 +1,70 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.product.entity.Category; -import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.entity.SaleState; -import com.example.eightyage.domain.product.repository.ProductBulkRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.UUID; - -@SpringBootTest -@ActiveProfiles("ci") -public class ProductBulkTest { - - @Autowired - private ProductBulkRepository productBulkRepository; - - @Test - void 제품_더미데이터_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 0; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - Random random = new Random(); - - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - Category randomCategory = Category.values()[random.nextInt(Category.values().length)]; - Product product = Product.builder() - .category(randomCategory) - .name(UUID.randomUUID().toString()) - .saleState(random.nextBoolean() ? SaleState.FOR_SALE : SaleState.SOLD_OUT) - .build(); - - batchList.add(product); - - if (batchList.size() == insertCount) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - - sleep(500); - } - } - - if (!batchList.isEmpty()) { - productBulkRepository.bulkInsertProduct(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.product.entity.Category; +//import com.example.eightyage.domain.product.entity.Product; +//import com.example.eightyage.domain.product.entity.SaleState; +//import com.example.eightyage.domain.product.repository.ProductBulkRepository; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Random; +//import java.util.UUID; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class ProductBulkTest { +// +// @Autowired +// private ProductBulkRepository productBulkRepository; +// +// @Test +// void 제품_더미데이터_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 0; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// Random random = new Random(); +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// Category randomCategory = Category.values()[random.nextInt(Category.values().length)]; +// Product product = Product.builder() +// .category(randomCategory) +// .name(UUID.randomUUID().toString()) +// .saleState(random.nextBoolean() ? SaleState.FOR_SALE : SaleState.SOLD_OUT) +// .build(); +// +// batchList.add(product); +// +// if (batchList.size() == insertCount) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// +// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// productBulkRepository.bulkInsertProduct(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 38ee354..2e7a9dd 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -1,65 +1,65 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.review.entity.Review; -import com.example.eightyage.domain.review.repository.ReviewBulkRepository; -import com.example.eightyage.domain.user.entity.User; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -@SpringBootTest -@ActiveProfiles("ci") -public class ReviewBulkTest { - - @Autowired - private ReviewBulkRepository reviewBulkRepository; - - @Test - void 리뷰_더미데이터_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 0; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - Random random = new Random(); - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - User user = User.builder().id(1L).build(); - Product product = new Product((long) (random.nextInt(100) + 1)); - - Review review = new Review(user, product, random.nextDouble() * 5, "content" + i); - batchList.add(review); - - if (batchList.size() == insertCount) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - -// sleep(500); - } - } - - if (!batchList.isEmpty()) { - reviewBulkRepository.bulkInsertReviews(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.product.entity.Product; +//import com.example.eightyage.domain.review.entity.Review; +//import com.example.eightyage.domain.review.repository.ReviewBulkRepository; +//import com.example.eightyage.domain.user.entity.User; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Random; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class ReviewBulkTest { +// +// @Autowired +// private ReviewBulkRepository reviewBulkRepository; +// +// @Test +// void 리뷰_더미데이터_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 0; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// Random random = new Random(); +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// User user = User.builder().id(1L).build(); +// Product product = new Product((long) (random.nextInt(100) + 1)); +// +// Review review = new Review(user, product, random.nextDouble() * 5, "content" + i); +// batchList.add(review); +// +// if (batchList.size() == insertCount) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// +//// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// reviewBulkRepository.bulkInsertReviews(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 4a8c987..98d40f8 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -1,65 +1,65 @@ -package com.example.eightyage.bulk; - -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.userrole.UserRole; -import com.example.eightyage.domain.user.repository.UserBulkRepository; -import com.example.eightyage.global.util.RandomCodeGenerator; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.util.ArrayList; -import java.util.List; - -@SpringBootTest -@ActiveProfiles("ci") -public class UserBulkTest { - - @Autowired - private UserBulkRepository userBulkRepository; - - @Test - void 유저_데이터_생성() { - - int insertCount; - - if ("ci".equals(System.getProperty("spring.profiles.active"))) { - insertCount = 1; // CI에서는 데이터 적게 - } else { - insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 - } - - List batchList = new ArrayList<>(); - - for (int i = 0; i < insertCount; i++) { - User user = User.builder() - .email(RandomCodeGenerator.generateCouponCode(8) + "@email.com") - .nickname("nickname" + i) - .password("password") - .userRole(UserRole.ROLE_USER) - .build(); - batchList.add(user); - - if (batchList.size() == insertCount) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - -// sleep(500); - } - } - - if (!batchList.isEmpty()) { - userBulkRepository.bulkInsertUsers(batchList); - batchList.clear(); - } - } - - private static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} +//package com.example.eightyage.bulk; +// +//import com.example.eightyage.domain.user.entity.User; +//import com.example.eightyage.domain.user.userrole.UserRole; +//import com.example.eightyage.domain.user.repository.UserBulkRepository; +//import com.example.eightyage.global.util.RandomCodeGenerator; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.context.ActiveProfiles; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@SpringBootTest +//@ActiveProfiles("ci") +//public class UserBulkTest { +// +// @Autowired +// private UserBulkRepository userBulkRepository; +// +// @Test +// void 유저_데이터_생성() { +// +// int insertCount; +// +// if ("ci".equals(System.getProperty("spring.profiles.active"))) { +// insertCount = 1; // CI에서는 데이터 적게 +// } else { +// insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 +// } +// +// List batchList = new ArrayList<>(); +// +// for (int i = 0; i < insertCount; i++) { +// User user = User.builder() +// .email(RandomCodeGenerator.generateCouponCode(8) + "@email.com") +// .nickname("nickname" + i) +// .password("password") +// .userRole(UserRole.ROLE_USER) +// .build(); +// batchList.add(user); +// +// if (batchList.size() == insertCount) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// +//// sleep(500); +// } +// } +// +// if (!batchList.isEmpty()) { +// userBulkRepository.bulkInsertUsers(batchList); +// batchList.clear(); +// } +// } +// +// private static void sleep(int millis) { +// try { +// Thread.sleep(millis); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +//} From d2b62244d7a693286a335bd5fdefc881873c24cf Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Fri, 28 Mar 2025 23:30:31 +0900 Subject: [PATCH 149/164] =?UTF-8?q?refactor(config):=20S3=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20application=20yml?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 변경사항 - application.yml, application-local.yml, application-ci.yml 설정 정리 - S3 bucket 이름과 region은 서비스단에서 상수 처리 - 중복 spring 블록 제거 및 YAML 구조 개선 - ci.yml 디버그 모드 해제 --- .github/workflows/ci.yml | 7 +++--- .../product/service/ProductImageService.java | 15 ++++-------- .../eightyage/global/config/S3Config.java | 10 ++++---- src/main/resources/application-ci.yml | 21 ++++++++-------- src/main/resources/application-local.yml | 24 ++++++++++--------- src/main/resources/application.yml | 16 +------------ 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a531fc..58dc29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,11 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: Clean Gradle Cache (Optional but good) + - name: Clean Gradle Cache run: ./gradlew clean --refresh-dependencies - - name: Test And Build with Gradle (Debug Mode) + + - name: Test And Build with Gradle env: JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - run: ./gradlew build --stacktrace --info -Dspring.profiles.active=ci + run: ./gradlew build -Dspring.profiles.active=ci diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java index d5ed35e..0a3599c 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductImageService.java @@ -3,7 +3,6 @@ import com.example.eightyage.domain.product.entity.Product; import com.example.eightyage.domain.product.entity.ProductImage; import com.example.eightyage.domain.product.repository.ProductImageRepository; -import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.global.exception.NotFoundException; import com.example.eightyage.global.exception.ProductImageUploadException; import lombok.RequiredArgsConstructor; @@ -14,9 +13,8 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + import java.io.IOException; -import java.time.LocalDateTime; import java.util.UUID; @Service @@ -27,11 +25,8 @@ public class ProductImageService { private final ProductImageRepository productImageRepository; private final ProductService productService; - @Value("${aws.s3.bucket}") - private String bucket; - - @Value("${aws.region}") - private String region; + private static final String BUCKET_NAME = "my-gom-bucket"; + private static final String REGION = "ap-northeast-2"; // 제품 이미지 업로드 @Transactional @@ -42,7 +37,7 @@ public String uploadImage(Long productId, MultipartFile file) { // S3에 업로드 s3Client.putObject( PutObjectRequest.builder() - .bucket(bucket) + .bucket(BUCKET_NAME) .key(fileName) .contentType(file.getContentType()) .build(), @@ -50,7 +45,7 @@ public String uploadImage(Long productId, MultipartFile file) { ); // S3 이미지 URL 생성 - String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, fileName); + String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", BUCKET_NAME, REGION, fileName); // DB 저장 Product product = productService.findProductByIdOrElseThrow(productId); diff --git a/src/main/java/com/example/eightyage/global/config/S3Config.java b/src/main/java/com/example/eightyage/global/config/S3Config.java index b62e367..5d48b01 100644 --- a/src/main/java/com/example/eightyage/global/config/S3Config.java +++ b/src/main/java/com/example/eightyage/global/config/S3Config.java @@ -11,19 +11,19 @@ @Configuration public class S3Config { - @Value("${aws.region}") - private String region; + private static final String REGION = "ap-northeast-2"; - @Value("${aws.credentials.access-key}") + @Value("${aws.access-key}") private String accessKey; - @Value("${aws.credentials.secret-key}") + @Value("${aws.secret-key}") private String secretKey; + @Bean public S3Client s3Client() { return S3Client.builder() - .region(Region.of(region)) + .region(Region.of(REGION)) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(accessKey, secretKey) )) diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 1110e9e..17459d6 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -3,27 +3,26 @@ server: spring: datasource: - url: jdbc:mysql://mysql:3306/team8_test + url: jdbc:mysql://localhost:3306/team8_test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect show_sql: true format_sql: true + + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + jwt: secret: - key: ${JWT_SECRET_KEY} - -aws: - credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} - region: ap-northeast-2 - s3: - bucket: my-gom-bucket \ No newline at end of file + key: ${JWT_SECRET_KEY} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 610611b..f89a31b 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,18 +1,20 @@ spring: + config: + import: optional:file:.env[.properties] + cloud: aws: credentials: access-key: ${AWS_ACCESS_KEY} secret-key: ${AWS_SECRET_KEY} - region: - static: ap-northeast-2 - s3: - bucket: my-gom-bucket -aws: - credentials: - access-key: ${AWS_ACCESS_KEY} - secret-key: ${AWS_SECRET_KEY} - region: ap-northeast-2 - s3: - bucket: my-gom-bucket \ No newline at end of file + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + data: + redis: + host: localhost + port: 6379 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f87da91..768de84 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,24 +9,9 @@ server: session: timeout: 1800 -spring: - config: - import: optional:file:.env[.properties] - data: - redis: - host: localhost - port: 6379 - - application: name: eightyage - datasource: - url: ${DB_URL} - username: ${DB_USER} - password: ${DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - jpa: hibernate: ddl-auto: update @@ -40,3 +25,4 @@ spring: jwt: secret: key: ${JWT_SECRET_KEY} + From 23f450048e2ce9615e1cb7c63af9e74061d7c41e Mon Sep 17 00:00:00 2001 From: queenriwon Date: Sat, 29 Mar 2025 04:39:02 +0900 Subject: [PATCH 150/164] =?UTF-8?q?refactor(global):=20=EB=8D=94=EB=AF=B8?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 수정한 것 - 더미데이터 랜덤수 생성을 리팩토링 --- .../domain/product/repository/ProductBulkRepository.java | 7 ++----- .../domain/review/repository/ReviewBulkRepository.java | 8 +++----- .../java/com/example/eightyage/bulk/ProductBulkTest.java | 4 ++-- .../java/com/example/eightyage/bulk/ReviewBulkTest.java | 4 ++-- .../java/com/example/eightyage/bulk/UserBulkTest.java | 4 ++-- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java index 272b3e8..64f1e61 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductBulkRepository.java @@ -20,13 +20,10 @@ public class ProductBulkRepository { public void bulkInsertProduct(List products) { String sql = "INSERT INTO product (category, name, sale_state) values (?, ?, ?)"; - Random random = new Random(); - jdbcTemplate.batchUpdate(sql, products, BATCH_SIZE, (ps, argument) -> { - Category randomCategory = Category.values()[random.nextInt(Category.values().length)]; - ps.setString(1, randomCategory.name()); + ps.setString(1, argument.getCategory().name()); ps.setString(2, argument.getName()); - ps.setString(3, random.nextBoolean() ? SaleState.FOR_SALE.name() : SaleState.SOLD_OUT.name()); + ps.setString(3, argument.getSaleState().name()); }); } } diff --git a/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java index 7b72dd5..c9bf85a 100644 --- a/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java +++ b/src/main/java/com/example/eightyage/domain/review/repository/ReviewBulkRepository.java @@ -18,12 +18,10 @@ public class ReviewBulkRepository { public void bulkInsertReviews(List reviews) { String sql = "INSERT INTO review (user_id, product_id, score) values (?, ?, ?)"; - Random random = new Random(); - jdbcTemplate.batchUpdate(sql, reviews, BATCH_SIZE, (ps, argument) -> { - ps.setInt(1, 1); - ps.setInt(2, random.nextInt(1000) + 1); - ps.setDouble(3, random.nextDouble() * 5); + ps.setLong(1, argument.getUser().getId()); + ps.setLong(2, argument.getProduct().getId()); + ps.setDouble(3, argument.getScore()); }); } diff --git a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java index 3d8c7db..810f73d 100644 --- a/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ProductBulkTest.java @@ -27,7 +27,7 @@ // int insertCount; // // if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 0; // CI에서는 데이터 적게 +// insertCount = 100; // CI에서는 데이터 적게 // } else { // insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 // } @@ -46,7 +46,7 @@ // // batchList.add(product); // -// if (batchList.size() == insertCount) { +// if (batchList.size() == insertCount / 100) { // productBulkRepository.bulkInsertProduct(batchList); // batchList.clear(); // diff --git a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java index 2e7a9dd..9ecb4e1 100644 --- a/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/ReviewBulkTest.java @@ -26,7 +26,7 @@ // int insertCount; // // if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 0; // CI에서는 데이터 적게 +// insertCount = 100; // CI에서는 데이터 적게 // } else { // insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 // } @@ -41,7 +41,7 @@ // Review review = new Review(user, product, random.nextDouble() * 5, "content" + i); // batchList.add(review); // -// if (batchList.size() == insertCount) { +// if (batchList.size() == insertCount / 100) { // reviewBulkRepository.bulkInsertReviews(batchList); // batchList.clear(); // diff --git a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java index 98d40f8..51ec056 100644 --- a/src/test/java/com/example/eightyage/bulk/UserBulkTest.java +++ b/src/test/java/com/example/eightyage/bulk/UserBulkTest.java @@ -25,7 +25,7 @@ // int insertCount; // // if ("ci".equals(System.getProperty("spring.profiles.active"))) { -// insertCount = 1; // CI에서는 데이터 적게 +// insertCount = 100; // CI에서는 데이터 적게 // } else { // insertCount = 1000000; // 로컬, 개발 서버 등에서는 많게 // } @@ -41,7 +41,7 @@ // .build(); // batchList.add(user); // -// if (batchList.size() == insertCount) { +// if (batchList.size() == insertCount / 100) { // userBulkRepository.bulkInsertUsers(batchList); // batchList.clear(); // From 80f2c340d3fa7724e48ab520c6ee4e25573c0d4f Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:26:06 +0900 Subject: [PATCH 151/164] =?UTF-8?q?refactor(coupon)=20event=20=E2=86=92=20?= =?UTF-8?q?coupon,=20coupon=20=E2=86=92=20issuedCoupon=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 32 ++++-- .../controller/IssuedCouponController.java | 36 ++++++ .../coupon/couponstate/CouponState.java | 6 + .../domain/coupon/dto/request/.gitkeep | 0 .../dto/request/CouponRequestDto.java} | 4 +- .../dto/response/CouponResponseDto.java | 26 ++--- .../dto/response/IssuedCouponResponseDto.java | 29 +++++ .../domain/coupon/entity/Coupon.java | 70 +++++++----- .../domain/coupon/entity/CouponState.java | 6 - .../domain/coupon/entity/IssuedCoupon.java | 56 +++++++++ .../coupon/repository/CouponRepository.java | 7 -- .../repository/IssuedCouponRepository.java | 14 +++ .../domain/coupon/service/CouponService.java | 107 +++++++++--------- .../coupon/service/IssuedCouponService.java | 102 +++++++++++++++++ .../domain/coupon/status/Status.java | 6 + .../event/controller/EventController.java | 41 ------- .../event/dto/response/EventResponseDto.java | 27 ----- .../eightyage/domain/event/entity/Event.java | 72 ------------ .../domain/event/entity/EventState.java | 6 - .../event/repository/EventRepository.java | 9 -- .../domain/event/service/EventService.java | 97 ---------------- 21 files changed, 376 insertions(+), 377 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep rename src/main/java/com/example/eightyage/domain/{event/dto/request/EventRequestDto.java => coupon/dto/request/CouponRequestDto.java} (90%) create mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/status/Status.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/controller/EventController.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/entity/Event.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/entity/EventState.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/service/EventService.java diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index cdc3b69..ea9a8c2 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,11 +1,15 @@ package com.example.eightyage.domain.coupon.controller; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.service.CouponService; import com.example.eightyage.global.dto.AuthUser; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -16,21 +20,25 @@ public class CouponController { private final CouponService couponService; - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); + @PreAuthorize("hasRole('ADMIN')") + @PostMapping("/v1/events") + public ResponseEntity createCoupon(@Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.saveCoupon(couponRequestDto)); } - @GetMapping("/v1/coupons/my") - public ResponseEntity> getMyCoupons( - @AuthenticationPrincipal AuthUser authUser, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); + @GetMapping("/v1/events") + public ResponseEntity> getCoupons(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(couponService.getCoupons(page, size)); } - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { - return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); + @GetMapping("/v1/events/{eventId}") + public ResponseEntity getCoupon(@PathVariable long eventId) { + return ResponseEntity.ok(couponService.getCoupon(eventId)); + } + + @PreAuthorize("hasRole('ADMIN')") + @PatchMapping("/v1/events/{eventId}") + public ResponseEntity updateCoupon(@PathVariable long eventId, @Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.updateCoupon(eventId, couponRequestDto)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java new file mode 100644 index 0000000..08fa202 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.coupon.controller; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.service.IssuedCouponService; +import com.example.eightyage.global.dto.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class IssuedCouponController { + + private final IssuedCouponService issuedCouponService; + + @PostMapping("/v1/events/{eventId}/coupons") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { + return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, eventId)); + } + + @GetMapping("/v1/coupons/my") + public ResponseEntity> getMyCoupons( + @AuthenticationPrincipal AuthUser authUser, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(issuedCouponService.getMyCoupons(authUser, page, size)); + } + + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { + return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, couponId)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java new file mode 100644 index 0000000..1106d56 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.coupon.couponstate; + +public enum CouponState { + VALID, + INVALID +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java similarity index 90% rename from src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java rename to src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java index 381bd19..6615147 100644 --- a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.event.dto.request; +package com.example.eightyage.domain.coupon.dto.request; import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.Min; @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor -public class EventRequestDto { +public class CouponRequestDto { @NotBlank(message = ValidationMessage.NOT_BLANK_EVENT_NAME) private String name; diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java index d1eca80..3bb6bd8 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.coupon.dto.response; -import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.couponstate.CouponState; import lombok.Getter; import java.time.LocalDateTime; @@ -8,22 +8,20 @@ @Getter public class CouponResponseDto { - private final String couponCode; + private final String name; + private final String description; + private final int quantity; + private final LocalDateTime startDate; + private final LocalDateTime endDate; private final CouponState state; - private final String username; - private final String eventname; - private final LocalDateTime startAt; - private final LocalDateTime endAt; - public CouponResponseDto(String couponCode, CouponState state, - String username, String eventname, - LocalDateTime startAt, LocalDateTime endAt) { - this.couponCode = couponCode; + public CouponResponseDto(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate, CouponState state) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; this.state = state; - this.username = username; - this.eventname = eventname; - this.startAt = startAt; - this.endAt = endAt; } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java new file mode 100644 index 0000000..c97fb99 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.coupon.dto.response; + +import com.example.eightyage.domain.coupon.status.Status; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class IssuedCouponResponseDto { + + private final String serialCode; + private final Status status; + private final String username; + private final String eventname; + + private final LocalDateTime startAt; + private final LocalDateTime endAt; + + public IssuedCouponResponseDto(String serialCode, Status status, + String username, String eventname, + LocalDateTime startAt, LocalDateTime endAt) { + this.serialCode = serialCode; + this.status = status; + this.username = username; + this.eventname = eventname; + this.startAt = startAt; + this.endAt = endAt; + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java index 76b1b10..8699d08 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -1,56 +1,66 @@ package com.example.eightyage.domain.coupon.entity; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.entity.TimeStamped; -import com.example.eightyage.global.util.RandomCodeGenerator; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity -@Builder @Getter @NoArgsConstructor -@AllArgsConstructor public class Coupon extends TimeStamped { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @Column(unique = true) - private String couponCode; - + private String name; + private String description; + private int quantity; + @Column(name="start_at") + private LocalDateTime startDate; + @Column(name = "end_at") + private LocalDateTime endDate; @Enumerated(EnumType.STRING) private CouponState state; - @ManyToOne(fetch = FetchType.LAZY) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - private Event event; - - public static Coupon create(User user, Event event) { - return Coupon.builder() - .couponCode(RandomCodeGenerator.generateCouponCode(10)) - .state(CouponState.VALID) - .user(user) - .event(event) - .build(); + public Coupon(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; } public CouponResponseDto toDto() { return new CouponResponseDto( - this.couponCode, - this.state, - this.user.getNickname(), - this.event.getName(), - this.event.getStartDate(), - this.event.getEndDate() + this.getName(), + this.getDescription(), + this.getQuantity(), + this.getStartDate(), + this.getEndDate(), + this.getState() ); } + + public void update(CouponRequestDto couponRequestDto) { + this.name = couponRequestDto.getName(); + this.description = couponRequestDto.getDescription(); + this.quantity = couponRequestDto.getQuantity(); + this.startDate = couponRequestDto.getStartDate(); + this.endDate = couponRequestDto.getEndDate(); + } + + public boolean isValidAt(LocalDateTime time) { + return (startDate.isBefore(time) || startDate.isEqual(time)) && (endDate.isAfter(time) || endDate.isEqual(time)); + } + + public void updateStateAt(LocalDateTime time) { + CouponState newState = isValidAt(time) ? CouponState.VALID : CouponState.INVALID; + this.state = newState; + } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java deleted file mode 100644 index 221a935..0000000 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.coupon.entity; - -public enum CouponState { - VALID, - INVALID -} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java new file mode 100644 index 0000000..0926367 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java @@ -0,0 +1,56 @@ +package com.example.eightyage.domain.coupon.entity; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.status.Status; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.entity.TimeStamped; +import com.example.eightyage.global.util.RandomCodeGenerator; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class IssuedCoupon extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String serialCode; + + @Enumerated(EnumType.STRING) + private Status status; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private Coupon coupon; + + public static IssuedCoupon create(User user, Coupon coupon) { + return IssuedCoupon.builder() + .serialCode(RandomCodeGenerator.generateCouponCode(10)) + .status(Status.VALID) + .user(user) + .coupon(coupon) + .build(); + } + + public IssuedCouponResponseDto toDto() { + return new IssuedCouponResponseDto( + this.serialCode, + this.status, + this.user.getNickname(), + this.coupon.getName(), + this.coupon.getStartDate(), + this.coupon.getEndDate() + ); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java index a6c5dbd..d617d00 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java @@ -1,16 +1,9 @@ package com.example.eightyage.domain.coupon.repository; import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface CouponRepository extends JpaRepository { - boolean existsByUserIdAndEventId(Long userId, Long eventId); - Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java new file mode 100644 index 0000000..210b7cd --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -0,0 +1,14 @@ +package com.example.eightyage.domain.coupon.repository; + +import com.example.eightyage.domain.coupon.entity.IssuedCoupon; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IssuedCouponRepository extends JpaRepository { + boolean existsByUserIdAndEventId(Long userId, Long eventId); + Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 7f45cde..9932875 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,19 +1,13 @@ package com.example.eightyage.domain.coupon.service; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.service.EventService; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.ErrorMessage; -import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; -import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -21,83 +15,88 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import java.util.concurrent.TimeUnit; +import java.time.LocalDateTime; @Service @RequiredArgsConstructor public class CouponService { private final CouponRepository couponRepository; - private final EventService eventService; private final StringRedisTemplate stringRedisTemplate; + private final RedissonClient redissonClient; private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; private static final String EVENT_LOCK_PREFIX = "event:lock:"; - private final RedissonClient redissonClient; - public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { + Coupon coupon = new Coupon( + couponRequestDto.getName(), + couponRequestDto.getDescription(), + couponRequestDto.getQuantity(), + couponRequestDto.getStartDate(), + couponRequestDto.getEndDate() + ); - RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); - boolean isLocked = false; + checkEventState(coupon); - try { - isLocked = rLock.tryLock(3, 10, TimeUnit.SECONDS); // 3초 안에 락을 획득, 10초 뒤에는 자동 해제 + Coupon savedCoupon = couponRepository.save(coupon); - if (!isLocked) { - throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 - } + stringRedisTemplate.opsForValue().set("event:quantity:" + savedCoupon.getId(), String.valueOf(savedCoupon.getQuantity())); - Event event = eventService.getValidEventOrThrow(eventId); + return savedCoupon.toDto(); + } - if (couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } + public Page getCoupons(int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page events = couponRepository.findAll(pageable); - Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); - if (remain == 0 || remain < 0) { - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + // 모든 events들 checkState로 state 상태 갱신하기 + events.forEach(this::checkEventState); - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); - couponRepository.save(coupon); + return events.map(Coupon::toDto); + } - return coupon.toDto(); + public CouponResponseDto getCoupon(long eventId) { + Coupon coupon = findByIdOrElseThrow(eventId); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new BadRequestException(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage()); - } finally { - if (isLocked) { - rLock.unlock(); - } - } + checkEventState(coupon); + + return coupon.toDto(); } - public Page getMyCoupons(AuthUser authUser, int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + public CouponResponseDto updateCoupon(long eventId, CouponRequestDto couponRequestDto) { + Coupon coupon = findByIdOrElseThrow(eventId); + + coupon.update(couponRequestDto); + + checkEventState(coupon); - return coupons.map(Coupon::toDto); + return coupon.toDto(); } - public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = findByIdOrElseThrow(couponId); + private void checkEventState(Coupon coupon) { + CouponState prevState = coupon.getState(); + coupon.updateStateAt(LocalDateTime.now()); - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); + if(coupon.getState() != prevState) { + couponRepository.save(coupon); } + } + + public Coupon getValidCouponOrThrow(Long eventId) { + Coupon coupon = findByIdOrElseThrow(eventId); - if(!coupon.getState().equals(CouponState.VALID)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); + coupon.updateStateAt(LocalDateTime.now()); + + if(coupon.getState() != CouponState.VALID) { + throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); } - return coupon.toDto(); + return coupon; } - public Coupon findByIdOrElseThrow(Long couponId) { - return couponRepository.findById(couponId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + public Coupon findByIdOrElseThrow(Long eventId) { + return couponRepository.findById(eventId) + .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java new file mode 100644 index 0000000..53ef385 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -0,0 +1,102 @@ +package com.example.eightyage.domain.coupon.service; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.entity.IssuedCoupon; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; +import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; +import com.example.eightyage.global.exception.ForbiddenException; +import com.example.eightyage.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class IssuedCouponService { + + private final IssuedCouponRepository issuedCouponRepository; + private final CouponService couponService; + private final StringRedisTemplate stringRedisTemplate; + + private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; + private static final String EVENT_LOCK_PREFIX = "event:lock:"; + private final RedissonClient redissonClient; + + public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + + RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); + boolean isLocked = false; + + try { + isLocked = rLock.tryLock(3, 10, TimeUnit.SECONDS); // 3초 안에 락을 획득, 10초 뒤에는 자동 해제 + + if (!isLocked) { + throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 + } + + Coupon coupon = couponService.getValidCouponOrThrow(eventId); + + if (issuedCouponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); + } + + Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); + if (remain == 0 || remain < 0) { + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); + } + stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + + // 쿠폰 발급 및 저장 + IssuedCoupon issuedCoupon = IssuedCoupon.create(User.fromAuthUser(authUser), coupon); + issuedCouponRepository.save(issuedCoupon); + + return issuedCoupon.toDto(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BadRequestException(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage()); + } finally { + if (isLocked) { + rLock.unlock(); + } + } + } + + public Page getMyCoupons(AuthUser authUser, int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page coupons = issuedCouponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + + return coupons.map(IssuedCoupon::toDto); + } + + public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long couponId) { + IssuedCoupon issuedCoupon = findByIdOrElseThrow(couponId); + + if(!issuedCoupon.getUser().equals(User.fromAuthUser(authUser))) { + throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); + } + + if(!issuedCoupon.getStatus().equals(CouponState.VALID)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); + } + + return issuedCoupon.toDto(); + } + + public IssuedCoupon findByIdOrElseThrow(Long couponId) { + return issuedCouponRepository.findById(couponId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/status/Status.java b/src/main/java/com/example/eightyage/domain/coupon/status/Status.java new file mode 100644 index 0000000..cfd21f3 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/status/Status.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.coupon.status; + +public enum Status { + VALID, + INVALID, +} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java deleted file mode 100644 index 17c8a81..0000000 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.eightyage.domain.event.controller; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.service.EventService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class EventController { - - private final EventService eventService; - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/v1/events") - public ResponseEntity createEvent(@Valid @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); - } - - @GetMapping("/v1/events") - public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(eventService.getEvents(page, size)); - } - - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getEvent(@PathVariable long eventId) { - return ResponseEntity.ok(eventService.getEvent(eventId)); - } - - @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @Valid @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java b/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java deleted file mode 100644 index 98edeb2..0000000 --- a/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.eightyage.domain.event.dto.response; - -import com.example.eightyage.domain.event.entity.EventState; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -public class EventResponseDto { - - private final String name; - private final String description; - private final int quantity; - private final LocalDateTime startDate; - private final LocalDateTime endDate; - private final EventState state; - - - public EventResponseDto(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate, EventState state) { - this.name = name; - this.description = description; - this.quantity = quantity; - this.startDate = startDate; - this.endDate = endDate; - this.state = state; - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/Event.java b/src/main/java/com/example/eightyage/domain/event/entity/Event.java deleted file mode 100644 index 7b2b560..0000000 --- a/src/main/java/com/example/eightyage/domain/event/entity/Event.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.eightyage.domain.event.entity; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.global.entity.TimeStamped; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Entity -@Getter -@NoArgsConstructor -public class Event extends TimeStamped { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - private String description; - private int quantity; - - @Column(name="start_at") - private LocalDateTime startDate; - @Column(name = "end_at") - private LocalDateTime endDate; - - @Enumerated(EnumType.STRING) - private EventState state; - - public Event(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate) { - this.name = name; - this.description = description; - this.quantity = quantity; - this.startDate = startDate; - this.endDate = endDate; - } - - public void setState(EventState state) { - this.state = state; - } - - public EventResponseDto toDto() { - return new EventResponseDto( - this.getName(), - this.getDescription(), - this.getQuantity(), - this.getStartDate(), - this.getEndDate(), - this.getState() - ); - } - - public void update(EventRequestDto eventRequestDto) { - this.name = eventRequestDto.getName(); - this.description = eventRequestDto.getDescription(); - this.quantity = eventRequestDto.getQuantity(); - this.startDate = eventRequestDto.getStartDate(); - this.endDate = eventRequestDto.getEndDate(); - } - - public boolean isValidAt(LocalDateTime time) { - return (startDate.isBefore(time) || startDate.isEqual(time)) && (endDate.isAfter(time) || endDate.isEqual(time)); - } - - public void updateStateAt(LocalDateTime time) { - EventState newState = isValidAt(time) ? EventState.VALID : EventState.INVALID; - this.state = newState; - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/EventState.java b/src/main/java/com/example/eightyage/domain/event/entity/EventState.java deleted file mode 100644 index 75bb82f..0000000 --- a/src/main/java/com/example/eightyage/domain/event/entity/EventState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.event.entity; - -public enum EventState { - VALID, - INVALID -} diff --git a/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java b/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java deleted file mode 100644 index f6ee989..0000000 --- a/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.eightyage.domain.event.repository; - -import com.example.eightyage.domain.event.entity.Event; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface EventRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java deleted file mode 100644 index 91c9b5f..0000000 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.entity.EventState; -import com.example.eightyage.domain.event.repository.EventRepository; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - -@Service -@RequiredArgsConstructor -public class EventService { - - private final EventRepository eventRepository; - private final StringRedisTemplate stringRedisTemplate; - - public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { - Event event = new Event( - eventRequestDto.getName(), - eventRequestDto.getDescription(), - eventRequestDto.getQuantity(), - eventRequestDto.getStartDate(), - eventRequestDto.getEndDate() - ); - - checkEventState(event); - - Event savedEvent = eventRepository.save(event); - - stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); - - return savedEvent.toDto(); - } - - public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page events = eventRepository.findAll(pageable); - - // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); - - return events.map(Event::toDto); - } - - public EventResponseDto getEvent(long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - return event.toDto(); - } - - public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = findByIdOrElseThrow(eventId); - - event.update(eventRequestDto); - - checkEventState(event); - - return event.toDto(); - } - - private void checkEventState(Event event) { - EventState prevState = event.getState(); - event.updateStateAt(LocalDateTime.now()); - - if(event.getState() != prevState) { - eventRepository.save(event); - } - } - - public Event getValidEventOrThrow(Long eventId) { - Event event = findByIdOrElseThrow(eventId); - - event.updateStateAt(LocalDateTime.now()); - - if(event.getState() != EventState.VALID) { - throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); - } - - return event; - } - - public Event findByIdOrElseThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); - } -} From d0206a2d0271892a422a3bb0393ce18c8b0a2b8b Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:39:46 +0900 Subject: [PATCH 152/164] =?UTF-8?q?refactor(issuedCoupon)=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 3 -- .../repository/IssuedCouponRepository.java | 5 +-- .../domain/coupon/service/CouponService.java | 31 ++++++++----------- .../coupon/service/IssuedCouponService.java | 15 ++++----- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index ea9a8c2..f19b6ca 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -2,15 +2,12 @@ import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.service.CouponService; -import com.example.eightyage.global.dto.AuthUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java index 210b7cd..0c84142 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -2,6 +2,7 @@ import com.example.eightyage.domain.coupon.entity.IssuedCoupon; import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.status.Status; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,6 +10,6 @@ @Repository public interface IssuedCouponRepository extends JpaRepository { - boolean existsByUserIdAndEventId(Long userId, Long eventId); - Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); + boolean existsByUserIdAndCouponId(Long userId, Long couponId); + Page findAllByUserIdAndStatus(Long userId, Status status, Pageable pageable); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 9932875..b71ddd1 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -8,7 +8,6 @@ import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.ErrorMessage; import lombok.RequiredArgsConstructor; -import org.redisson.api.RedissonClient; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -23,10 +22,6 @@ public class CouponService { private final CouponRepository couponRepository; private final StringRedisTemplate stringRedisTemplate; - private final RedissonClient redissonClient; - - private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; - private static final String EVENT_LOCK_PREFIX = "event:lock:"; public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { Coupon coupon = new Coupon( @@ -37,7 +32,7 @@ public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { couponRequestDto.getEndDate() ); - checkEventState(coupon); + checkCouponState(coupon); Coupon savedCoupon = couponRepository.save(coupon); @@ -51,30 +46,30 @@ public Page getCoupons(int page, int size) { Page events = couponRepository.findAll(pageable); // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); + events.forEach(this::checkCouponState); return events.map(Coupon::toDto); } - public CouponResponseDto getCoupon(long eventId) { - Coupon coupon = findByIdOrElseThrow(eventId); + public CouponResponseDto getCoupon(long couponId) { + Coupon coupon = findByIdOrElseThrow(couponId); - checkEventState(coupon); + checkCouponState(coupon); return coupon.toDto(); } - public CouponResponseDto updateCoupon(long eventId, CouponRequestDto couponRequestDto) { - Coupon coupon = findByIdOrElseThrow(eventId); + public CouponResponseDto updateCoupon(long couponId, CouponRequestDto couponRequestDto) { + Coupon coupon = findByIdOrElseThrow(couponId); coupon.update(couponRequestDto); - checkEventState(coupon); + checkCouponState(coupon); return coupon.toDto(); } - private void checkEventState(Coupon coupon) { + private void checkCouponState(Coupon coupon) { CouponState prevState = coupon.getState(); coupon.updateStateAt(LocalDateTime.now()); @@ -83,8 +78,8 @@ private void checkEventState(Coupon coupon) { } } - public Coupon getValidCouponOrThrow(Long eventId) { - Coupon coupon = findByIdOrElseThrow(eventId); + public Coupon getValidCouponOrThrow(Long couponId) { + Coupon coupon = findByIdOrElseThrow(couponId); coupon.updateStateAt(LocalDateTime.now()); @@ -95,8 +90,8 @@ public Coupon getValidCouponOrThrow(Long eventId) { return coupon; } - public Coupon findByIdOrElseThrow(Long eventId) { - return couponRepository.findById(eventId) + public Coupon findByIdOrElseThrow(Long couponId) { + return couponRepository.findById(couponId) .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index 53ef385..ca3c070 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -5,6 +5,7 @@ import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.coupon.status.Status; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; @@ -48,7 +49,7 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { Coupon coupon = couponService.getValidCouponOrThrow(eventId); - if (issuedCouponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), eventId)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } @@ -76,27 +77,27 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { public Page getMyCoupons(AuthUser authUser, int page, int size) { Pageable pageable = PageRequest.of(page-1, size); - Page coupons = issuedCouponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + Page coupons = issuedCouponRepository.findAllByUserIdAndStatus(authUser.getUserId(), Status.VALID, pageable); return coupons.map(IssuedCoupon::toDto); } - public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - IssuedCoupon issuedCoupon = findByIdOrElseThrow(couponId); + public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long issuedCouponId) { + IssuedCoupon issuedCoupon = findByIdOrElseThrow(issuedCouponId); if(!issuedCoupon.getUser().equals(User.fromAuthUser(authUser))) { throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); } - if(!issuedCoupon.getStatus().equals(CouponState.VALID)) { + if(issuedCoupon.getStatus().equals(Status.INVALID)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); } return issuedCoupon.toDto(); } - public IssuedCoupon findByIdOrElseThrow(Long couponId) { - return issuedCouponRepository.findById(couponId) + public IssuedCoupon findByIdOrElseThrow(Long issuedCouponId) { + return issuedCouponRepository.findById(issuedCouponId) .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); } } From afab07c7742c1bc200c55e01454f40d51febf042 Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:40:53 +0900 Subject: [PATCH 153/164] =?UTF-8?q?refactor(issuedCoupon)=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/repository/IssuedCouponRepository.java | 1 - .../eightyage/domain/coupon/service/IssuedCouponService.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java index 0c84142..e7b97da 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -1,7 +1,6 @@ package com.example.eightyage.domain.coupon.repository; import com.example.eightyage.domain.coupon.entity.IssuedCoupon; -import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.status.Status; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index ca3c070..7b6afe0 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -2,7 +2,6 @@ import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.entity.IssuedCoupon; -import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; import com.example.eightyage.domain.coupon.entity.Coupon; import com.example.eightyage.domain.coupon.status.Status; From 78bc89e3b127af767a5cb77a0b6783d6a42767b7 Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:54:21 +0900 Subject: [PATCH 154/164] =?UTF-8?q?refactor(issuedCoupon)=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 16 ++++++++-------- .../controller/IssuedCouponController.java | 12 ++++++------ .../coupon/service/IssuedCouponService.java | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index f19b6ca..5757dab 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -18,24 +18,24 @@ public class CouponController { private final CouponService couponService; @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/v1/events") + @PostMapping("/v1/coupons") public ResponseEntity createCoupon(@Valid @RequestBody CouponRequestDto couponRequestDto) { return ResponseEntity.ok(couponService.saveCoupon(couponRequestDto)); } - @GetMapping("/v1/events") + @GetMapping("/v1/coupons") public ResponseEntity> getCoupons(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return ResponseEntity.ok(couponService.getCoupons(page, size)); } - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getCoupon(@PathVariable long eventId) { - return ResponseEntity.ok(couponService.getCoupon(eventId)); + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@PathVariable long couponId) { + return ResponseEntity.ok(couponService.getCoupon(couponId)); } @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateCoupon(@PathVariable long eventId, @Valid @RequestBody CouponRequestDto couponRequestDto) { - return ResponseEntity.ok(couponService.updateCoupon(eventId, couponRequestDto)); + @PatchMapping("/v1/coupons/{couponId}") + public ResponseEntity updateCoupon(@PathVariable long couponId, @Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.updateCoupon(couponId, couponRequestDto)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java index 08fa202..d37aeea 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java @@ -16,9 +16,9 @@ public class IssuedCouponController { private final IssuedCouponService issuedCouponService; - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, eventId)); + @PostMapping("/v1/coupons/{couponId}/issues") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { + return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, couponId)); } @GetMapping("/v1/coupons/my") @@ -29,8 +29,8 @@ public ResponseEntity> getMyCoupons( return ResponseEntity.ok(issuedCouponService.getMyCoupons(authUser, page, size)); } - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { - return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, couponId)); + @GetMapping("/v1/coupons/{issuedCouponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long issuedCouponId) { + return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, issuedCouponId)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index 7b6afe0..913d046 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -34,9 +34,9 @@ public class IssuedCouponService { private static final String EVENT_LOCK_PREFIX = "event:lock:"; private final RedissonClient redissonClient; - public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long couponId) { - RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); + RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + couponId); boolean isLocked = false; try { @@ -46,17 +46,17 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 } - Coupon coupon = couponService.getValidCouponOrThrow(eventId); + Coupon coupon = couponService.getValidCouponOrThrow(couponId); - if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), eventId)) { + if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), couponId)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } - Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); + Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + couponId)); if (remain == 0 || remain < 0) { throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); } - stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + couponId); // 쿠폰 발급 및 저장 IssuedCoupon issuedCoupon = IssuedCoupon.create(User.fromAuthUser(authUser), coupon); From 2038dbbb890ab09360a6a8113db47bf1cb7922bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Sat, 29 Mar 2025 11:56:00 +0900 Subject: [PATCH 155/164] =?UTF-8?q?fix(product):=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EC=A4=91=20=EB=B0=9C=EA=B2=AC=EB=90=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/product/entity/Product.java | 1 + .../domain/product/service/ProductServiceTest.java | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/product/entity/Product.java b/src/main/java/com/example/eightyage/domain/product/entity/Product.java index 4c09ac5..020abe8 100644 --- a/src/main/java/com/example/eightyage/domain/product/entity/Product.java +++ b/src/main/java/com/example/eightyage/domain/product/entity/Product.java @@ -43,6 +43,7 @@ public class Product extends TimeStamped { private List reviews = new ArrayList<>(); @Temporal(TemporalType.TIMESTAMP) + @Column(nullable = true) private LocalDateTime deletedAt; @Builder diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java index da23f05..b7bff21 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -17,6 +17,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -71,7 +72,7 @@ class ProductServiceTest { reviewList.add(review1); reviewList.add(review2); - Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE, reviewList); + Product product = new Product(1L, "8자 주름 스킨", Category.SKINCARE, "8자 주름을 1자로 펴주는 퍼펙트 스킨", 20000, SaleState.FOR_SALE, reviewList, null); given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); @@ -92,7 +93,7 @@ class ProductServiceTest { given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); // when - ProductGetResponseDto responseDto = productService.findProductById(productId); + ProductGetResponseDto responseDto = productService.getProductById(productId); // then assertThat(responseDto.getProductName()).isEqualTo(product.getName()); @@ -114,9 +115,7 @@ class ProductServiceTest { productService.deleteProduct(productId); // then - verify(review1, times(1)).delete(); - verify(review2, times(1)).delete(); - - verify(product, times(1)).delete(); + verify(reviewRepository, times(1)).deleteAll(reviewList); + verify(product, times(1)).deleteProduct(); } } \ No newline at end of file From b30f7e9377ee19f4d10d8401cfe5efc4b8997167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Sat, 29 Mar 2025 11:56:12 +0900 Subject: [PATCH 156/164] =?UTF-8?q?fix(productImage):=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EC=A4=91=20=EB=B0=9C=EA=B2=AC=EB=90=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/service/ProductImageServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java index 77db7ee..bdbc2b1 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductImageServiceTest.java @@ -87,6 +87,6 @@ void setUp(){ productImageService.deleteImage(imageId); // then - verify(productImage, times(1)).delete(); + verify(productImageRepository, times(1)).delete(productImage); } } \ No newline at end of file From a3e6c5b9e7b0ab22689efdfd10911d22446bbc21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Sat, 29 Mar 2025 11:56:18 +0900 Subject: [PATCH 157/164] =?UTF-8?q?fix(review):=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EC=A4=91=20=EB=B0=9C=EA=B2=AC=EB=90=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eightyage/domain/review/service/ReviewServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java index 0b4a973..18bb5df 100644 --- a/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java @@ -137,7 +137,7 @@ class ReviewServiceTest { when(reviewRepository.findByProductIdAndProductDeletedAtIsNull(any(Long.class), eq(pageRequest))).thenReturn(reviewPage); // when - Page result = reviewService.findReviews(productId, pageRequest); + Page result = reviewService.getReviews(productId, pageRequest); // then assertNotNull(result); @@ -183,6 +183,6 @@ class ReviewServiceTest { reviewService.deleteReview(userId, reviewId); // then - verify(review, times(1)).delete(); + verify(reviewRepository, times(1)).delete(review); } } \ No newline at end of file From dbcd5145c581b5235c1191f6f09a91d75ead1bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Sat, 29 Mar 2025 12:13:33 +0900 Subject: [PATCH 158/164] =?UTF-8?q?feat(product):=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20productIm?= =?UTF-8?q?ageList=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=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 --- .../domain/product/dto/response/ProductGetResponseDto.java | 3 +++ .../domain/product/repository/ProductImageRepository.java | 5 ++++- .../eightyage/domain/product/service/ProductService.java | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java index 534ae84..60d93ab 100644 --- a/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/product/dto/response/ProductGetResponseDto.java @@ -1,12 +1,14 @@ package com.example.eightyage.domain.product.dto.response; import com.example.eightyage.domain.product.category.Category; +import com.example.eightyage.domain.product.entity.ProductImage; import com.example.eightyage.domain.product.salestate.SaleState; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import java.time.LocalDateTime; +import java.util.List; @Getter @Builder @@ -17,6 +19,7 @@ public class ProductGetResponseDto { private final Category category; private final Integer price; private final SaleState saleState; + private final List productImageList; private final LocalDateTime createdAt; private final LocalDateTime modifiedAt; } diff --git a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java index aa2c50e..9589344 100644 --- a/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java +++ b/src/main/java/com/example/eightyage/domain/product/repository/ProductImageRepository.java @@ -4,11 +4,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.awt.*; +import java.util.List; import java.util.Optional; public interface ProductImageRepository extends JpaRepository { @Query("SELECT pi FROM ProductImage pi WHERE pi.id = :imageId") Optional findById(Long imageId); + + @Query("SELECT pi.imageUrl FROM ProductImage pi WHERE pi.product.id = :productId") + List findProductImageByProductId(Long productId); } diff --git a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java index 01a81a4..bc36a31 100644 --- a/src/main/java/com/example/eightyage/domain/product/service/ProductService.java +++ b/src/main/java/com/example/eightyage/domain/product/service/ProductService.java @@ -5,6 +5,8 @@ import com.example.eightyage.domain.product.dto.response.*; import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.entity.ProductImage; +import com.example.eightyage.domain.product.repository.ProductImageRepository; import com.example.eightyage.domain.product.salestate.SaleState; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; @@ -30,6 +32,7 @@ public class ProductService { private final ProductRepository productRepository; private final ReviewRepository reviewRepository; + private final ProductImageRepository productImageRepository; private final SearchServiceV1 searchServiceV1; private final SearchServiceV2 searchServiceV2; private final SearchServiceV3 searchServiceV3; @@ -78,6 +81,7 @@ public ProductUpdateResponseDto updateProduct(Long productId, ProductUpdateReque @Transactional(readOnly = true) public ProductGetResponseDto getProductById(Long productId) { Product findProduct = findProductByIdOrElseThrow(productId); + List productImageList = productImageRepository.findProductImageByProductId(productId); return ProductGetResponseDto.builder() .productName(findProduct.getName()) @@ -85,6 +89,7 @@ public ProductGetResponseDto getProductById(Long productId) { .category(findProduct.getCategory()) .price(findProduct.getPrice()) .saleState(findProduct.getSaleState()) + .productImageList(productImageList) .createdAt(findProduct.getCreatedAt()) .modifiedAt(findProduct.getModifiedAt()) .build(); From 7ad2d7bb8ed7a09403af7dcb49299121e9caa794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=8C=E1=85=B5=E1=84=8B=E1=85=B3?= =?UTF-8?q?=E1=86=AB?= Date: Sat, 29 Mar 2025 12:21:46 +0900 Subject: [PATCH 159/164] =?UTF-8?q?fix(product):=20productImageList=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=ED=9B=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/service/ProductServiceTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java index b7bff21..aa91cb0 100644 --- a/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/product/service/ProductServiceTest.java @@ -7,6 +7,7 @@ import com.example.eightyage.domain.product.dto.response.ProductUpdateResponseDto; import com.example.eightyage.domain.product.category.Category; import com.example.eightyage.domain.product.entity.Product; +import com.example.eightyage.domain.product.repository.ProductImageRepository; import com.example.eightyage.domain.product.salestate.SaleState; import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.review.entity.Review; @@ -35,6 +36,9 @@ class ProductServiceTest { @Mock ProductRepository productRepository; + @Mock + ProductImageRepository productImageRepository; + @Mock ReviewRepository reviewRepository; @@ -89,8 +93,13 @@ class ProductServiceTest { void 제품_단건_조회_성공(){ // given Long productId = 1L; + List productImageList = new ArrayList<>(); + + productImageList.add("image1.png"); + productImageList.add("image2.png"); given(productRepository.findById(any(Long.class))).willReturn(Optional.of(product)); + given(productImageRepository.findProductImageByProductId(any(Long.class))).willReturn(productImageList); // when ProductGetResponseDto responseDto = productService.getProductById(productId); From 5c7b4ffdf8a1561bbd3720f40c21e68ec5a5467d Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Sun, 30 Mar 2025 00:05:49 +0900 Subject: [PATCH 160/164] =?UTF-8?q?chore(cd):=20CD=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20=EC=A4=80?= =?UTF-8?q?=EB=B9=84=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GitHub Actions용 cd.yml 작성 - Dockerfile 생성 - actuator 의존성 추가 - SecurityConfig에 actuator 접근 허용 설정 --- .github/workflows/cd.yml | 86 +++++++++++++++++++ Dockerfile | 12 +++ build.gradle | 2 + .../global/config/SecurityConfig.java | 1 + 4 files changed, 101 insertions(+) create mode 100644 .github/workflows/cd.yml create mode 100644 Dockerfile diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..ba0cfd5 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,86 @@ +name: Deploy to EC2 + +on: + push: + branches: [ "main", "test/deploy" ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker Build + run: docker build -t eightyage . + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: ECR Login + run: | + aws ecr get-login-password | \ + docker login --username AWS --password-stdin ${{ secrets.ECR_URI }} + + - name: Docker Images Check + run: docker images + + - name: ECR Push + run: | + docker tag eightyage:latest ${{ secrets.ECR_URI }} + docker push ${{ secrets.ECR_URI }} + + - name: Deploy on EC2 + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + envs: ECR_URI,DB_URL,DB_USER,DB_PASSWORD,JWT_SECRET_KEY,AWS_ACCESS_KEY,AWS_SECRET_KEY,REDIS_HOST + + script: | + export ECR_URI=${{ secrets.ECR_URI }} + export DB_URL=${{ secrets.DB_URL }} + export DB_USER=${{ secrets.DB_USER }} + export DB_PASSWORD=${{ secrets.DB_PASSWORD }} + export JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + export AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + export AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + export REDIS_HOST=${{ secrets.REDIS_HOST }} + + docker ps -q --filter ancestor=$ECR_URI | xargs -r docker stop + docker ps -aq --filter ancestor=$ECR_URI | xargs -r docker rm + + aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URI + docker pull $ECR_URI + docker run -d -p 8080:8080 \ + -e DB_URL=$DB_URL \ + -e DB_USER=$DB_USER \ + -e DB_PASSWORD=$DB_PASSWORD \ + -e JWT_SECRET_KEY=$JWT_SECRET_KEY \ + -e AWS_ACCESS_KEY=$AWS_ACCESS_KEY \ + -e AWS_SECRET_KEY=$AWS_SECRET_KEY \ + -e REDIS_HOST=$REDIS_HOST \ + $ECR_URI + + - name: Health Check + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + for i in {1..10}; do + echo "⏳ Health check attempt $i..." + if curl -f http://localhost:8080/actuator/health; then + echo "✅ Health check succeeded!" + exit 0 + fi + sleep 5 + done + echo "❌ Health check failed after multiple attempts" + exit 1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b62ead --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM gradle:8.6-jdk17 AS build +WORKDIR /app +COPY . . +RUN gradle clean build -x test + +FROM eclipse-temurin:17-jdk-alpine +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 + +ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7bfe89d..6d882b4 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,8 @@ dependencies { implementation 'org.redisson:redisson:3.23.5' testImplementation 'org.mockito:mockito-inline:5.2.0' + + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java index 2fe9756..50ee441 100644 --- a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java +++ b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java @@ -42,6 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .rememberMe(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers(request -> request.getRequestURI().startsWith("/api/v1/auth")).permitAll() + .requestMatchers("/actuator/**").permitAll() .anyRequest().authenticated() ) .build(); From 65d6f29291d4cb4372adb582a74774fae63a54c7 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Sun, 30 Mar 2025 00:09:30 +0900 Subject: [PATCH 161/164] =?UTF-8?q?chore(config):=20S3=EC=99=80=20Redis=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EA=B0=92=20=EC=99=B8=EB=B6=80=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S3Config accessKey, secretKey 설정값 수정 - RedissonConfig에 하드코딩된 host 값 제거 - @Value를 활용해 application 설정값으로 주입되도록 변경 --- .../example/eightyage/global/config/RedissonConfig.java | 8 ++++++-- .../com/example/eightyage/global/config/S3Config.java | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/eightyage/global/config/RedissonConfig.java b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java index 4586fb3..655646c 100644 --- a/src/main/java/com/example/eightyage/global/config/RedissonConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java @@ -5,15 +5,19 @@ import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; @Configuration public class RedissonConfig { + @Value("${spring.data.redis.host}") + private String redisHost; + @Bean public RedissonClient redisson() { Config config = new Config(); config.useSingleServer() - .setAddress("redis://localhost:6379"); - return Redisson.create(); + .setAddress("redis://" + redisHost + ":6379"); + return Redisson.create(config); } } diff --git a/src/main/java/com/example/eightyage/global/config/S3Config.java b/src/main/java/com/example/eightyage/global/config/S3Config.java index 5d48b01..db42729 100644 --- a/src/main/java/com/example/eightyage/global/config/S3Config.java +++ b/src/main/java/com/example/eightyage/global/config/S3Config.java @@ -13,10 +13,10 @@ public class S3Config { private static final String REGION = "ap-northeast-2"; - @Value("${aws.access-key}") + @Value("${AWS_ACCESS_KEY}") private String accessKey; - @Value("${aws.secret-key}") + @Value("${AWS_SECRET_KEY}") private String secretKey; From f6200a2b0827c644b6998a58974786b5a18c389f Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Sun, 30 Mar 2025 13:07:26 +0900 Subject: [PATCH 162/164] =?UTF-8?q?hotfix(config):=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=98=A4=EB=A5=98=20=EB=B0=8F=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=84=A4=EC=A0=95=20=EA=B8=B4=EA=B8=89=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 변경 내용 - application.yml 구조 정리 - application-local.yml에 AWS 설정 보완 - UserRole import 시 경로 오류 수정 - S3Config AWS 환경변수 키 이름 일치화 --- .../com/example/eightyage/global/config/S3Config.java | 4 ++-- src/main/resources/application-local.yml | 6 +++++- src/main/resources/application.yml | 4 ++-- .../domain/review/service/ReviewServiceTest.java | 11 +++++------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/eightyage/global/config/S3Config.java b/src/main/java/com/example/eightyage/global/config/S3Config.java index 5d48b01..db42729 100644 --- a/src/main/java/com/example/eightyage/global/config/S3Config.java +++ b/src/main/java/com/example/eightyage/global/config/S3Config.java @@ -13,10 +13,10 @@ public class S3Config { private static final String REGION = "ap-northeast-2"; - @Value("${aws.access-key}") + @Value("${AWS_ACCESS_KEY}") private String accessKey; - @Value("${aws.secret-key}") + @Value("${AWS_SECRET_KEY}") private String secretKey; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f89a31b..84e09b8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -7,6 +7,10 @@ spring: credentials: access-key: ${AWS_ACCESS_KEY} secret-key: ${AWS_SECRET_KEY} + region: + static: ap-northeast-2 + stack: + auto: false datasource: url: ${DB_URL} @@ -17,4 +21,4 @@ spring: data: redis: host: localhost - port: 6379 + port: 6379 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 768de84..d4d6498 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,7 @@ server: session: timeout: 1800 +spring: application: name: eightyage @@ -24,5 +25,4 @@ server: jwt: secret: - key: ${JWT_SECRET_KEY} - + key: ${JWT_SECRET_KEY} \ No newline at end of file diff --git a/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java index 18bb5df..acfaa82 100644 --- a/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java +++ b/src/test/java/com/example/eightyage/domain/review/service/ReviewServiceTest.java @@ -1,7 +1,6 @@ package com.example.eightyage.domain.review.service; import com.example.eightyage.domain.product.entity.Product; -import com.example.eightyage.domain.product.repository.ProductRepository; import com.example.eightyage.domain.product.service.ProductService; import com.example.eightyage.domain.review.dto.request.ReviewSaveRequestDto; import com.example.eightyage.domain.review.dto.request.ReviewUpdateRequestDto; @@ -11,18 +10,18 @@ import com.example.eightyage.domain.review.entity.Review; import com.example.eightyage.domain.review.repository.ReviewRepository; import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.domain.user.entity.UserRole; -import com.example.eightyage.domain.user.repository.UserRepository; import com.example.eightyage.domain.user.service.UserService; +import com.example.eightyage.domain.user.userrole.UserRole; import com.example.eightyage.global.exception.UnauthorizedException; 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.boot.autoconfigure.liquibase.LiquibaseProperties; -import org.springframework.data.domain.*; -import org.springframework.data.querydsl.QPageRequest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import java.util.ArrayList; import java.util.List; From ddce9d48f20f3b0e433a34762ee324bc4ab92975 Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Sun, 30 Mar 2025 14:27:27 +0900 Subject: [PATCH 163/164] =?UTF-8?q?chore(cd):=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EB=B0=8F=20=EC=8B=A4=ED=8C=A8=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EC=8A=AC=EB=9E=99=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B6=94=EA=B0=80=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 변경사항 - cd.yml에 배포 성공 및 실패 여부에 따른 슬랙 알림 추가 --- .github/workflows/cd.yml | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ba0cfd5..45f8d17 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -83,4 +83,40 @@ jobs: sleep 5 done echo "❌ Health check failed after multiple attempts" - exit 1 \ No newline at end of file + exit 1 + + - name: Notify Slack - 배포 성공 + if: success() + run: | + curl -X POST -H 'Content-type: application/json' \ + --data '{ + "text": "✅ *배포 성공!* 🎉", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*✅ 배포 성공했습니다!*\n\n*브랜치:* `${{ github.ref_name }}`\n" + } + } + ] + }' \ + ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Notify Slack - 배포 실패 + if: failure() + run: | + curl -X POST -H 'Content-type: application/json' \ + --data '{ + "text": "❌ *배포 실패!* 🔥", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*❌ 배포 실패했습니다!*\n\n*브랜치:* `${{ github.ref_name }}`\n" + } + } + ] + }' \ + ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file From 60e9bf9fcfc606f138a31c9fd9620d5bb24d9cfa Mon Sep 17 00:00:00 2001 From: 3uomlkh <3uomlkh@gmail.com> Date: Sun, 30 Mar 2025 14:37:48 +0900 Subject: [PATCH 164/164] =?UTF-8?q?chore(cd):=20application-prod.yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 변경사항 - 운영 환경에 필요한 application-prod.yml 파일 추가 - cd 대상 브랜치 main으로 한정 --- .github/workflows/cd.yml | 2 +- src/main/resources/application-prod.yml | 50 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-prod.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 45f8d17..9a3f5ae 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [ "main", "test/deploy" ] + branches: [ main ] jobs: deploy: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..3006731 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,50 @@ +server: + port: 8080 + servlet: + context-path: / + encoding: + charset: UTF-8 + enabled: true + force: true + session: + timeout: 1800 + +spring: + application: + name: eightyage + + data: + redis: + host: ${SPRING_DATA_REDIS_HOST} + port: 6379 + + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + show_sql: false + format_sql: true + use_sql_comments: false + dialect: org.hibernate.dialect.MySQLDialect + +jwt: + secret: + key: ${JWT_SECRET_KEY} + +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + show-details: always + security: + enabled: false \ No newline at end of file